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/bin/audit_event_type_spec.rb32
-rw-r--r--spec/bin/feature_flag_spec.rb10
-rw-r--r--spec/commands/sidekiq_cluster/cli_spec.rb49
-rw-r--r--spec/config/application_spec.rb2
-rw-r--r--spec/config/inject_enterprise_edition_module_spec.rb2
-rw-r--r--spec/config/mail_room_spec.rb2
-rw-r--r--spec/config/object_store_settings_spec.rb9
-rw-r--r--spec/config/settings_spec.rb2
-rw-r--r--spec/config/smime_signature_settings_spec.rb2
-rw-r--r--spec/contracts/consumer/.node-version1
-rw-r--r--spec/contracts/consumer/fixtures/project/merge_requests/diffs_batch.fixture.js (renamed from spec/contracts/consumer/fixtures/project/merge_request/diffs_batch.fixture.js)0
-rw-r--r--spec/contracts/consumer/fixtures/project/merge_requests/diffs_metadata.fixture.js (renamed from spec/contracts/consumer/fixtures/project/merge_request/diffs_metadata.fixture.js)0
-rw-r--r--spec/contracts/consumer/fixtures/project/merge_requests/discussions.fixture.js (renamed from spec/contracts/consumer/fixtures/project/merge_request/discussions.fixture.js)0
-rw-r--r--spec/contracts/consumer/fixtures/project/pipeline_schedules/update_pipeline_schedule.fixture.js (renamed from spec/contracts/consumer/fixtures/project/pipeline_schedule/update_pipeline_schedule.fixture.js)0
-rw-r--r--spec/contracts/consumer/fixtures/project/pipelines/create_a_new_pipeline.fixture.js (renamed from spec/contracts/consumer/fixtures/project/pipeline/create_a_new_pipeline.fixture.js)0
-rw-r--r--spec/contracts/consumer/fixtures/project/pipelines/delete_pipeline.fixture.js (renamed from spec/contracts/consumer/fixtures/project/pipeline/delete_pipeline.fixture.js)0
-rw-r--r--spec/contracts/consumer/fixtures/project/pipelines/get_list_project_pipelines.fixture.js (renamed from spec/contracts/consumer/fixtures/project/pipeline/get_list_project_pipelines.fixture.js)0
-rw-r--r--spec/contracts/consumer/fixtures/project/pipelines/get_pipeline_header_data.fixture.js (renamed from spec/contracts/consumer/fixtures/project/pipeline/get_pipeline_header_data.fixture.js)0
-rw-r--r--spec/contracts/consumer/package.json3
-rw-r--r--spec/contracts/consumer/specs/project/merge_requests/show.spec.js (renamed from spec/contracts/consumer/specs/project/merge_request/show.spec.js)28
-rw-r--r--spec/contracts/consumer/specs/project/pipeline_schedules/edit.spec.js (renamed from spec/contracts/consumer/specs/project/pipeline_schedule/edit.spec.js)6
-rw-r--r--spec/contracts/consumer/specs/project/pipelines/index.spec.js (renamed from spec/contracts/consumer/specs/project/pipeline/index.spec.js)6
-rw-r--r--spec/contracts/consumer/specs/project/pipelines/new.spec.js (renamed from spec/contracts/consumer/specs/project/pipeline/new.spec.js)6
-rw-r--r--spec/contracts/consumer/specs/project/pipelines/show.spec.js (renamed from spec/contracts/consumer/specs/project/pipeline/show.spec.js)6
-rw-r--r--spec/contracts/contracts/project/merge_requests/show/mergerequests#show-get_diffs_batch.json (renamed from spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_batch_endpoint.json)4
-rw-r--r--spec/contracts/contracts/project/merge_requests/show/mergerequests#show-get_diffs_metadata.json (renamed from spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_metadata_endpoint.json)6
-rw-r--r--spec/contracts/contracts/project/merge_requests/show/mergerequests#show-get_discussions.json (renamed from spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_discussions_endpoint.json)4
-rw-r--r--spec/contracts/contracts/project/pipeline_schedules/edit/pipelineschedules#edit-put_edit_a_pipeline_schedule.json (renamed from spec/contracts/contracts/project/pipeline_schedule/edit/pipelineschedules#edit-put_edit_a_pipeline_schedule.json)2
-rw-r--r--spec/contracts/contracts/project/pipelines/index/pipelines#index-get_list_project_pipelines.json (renamed from spec/contracts/contracts/project/pipeline/index/pipelines#index-get_list_project_pipelines.json)15
-rw-r--r--spec/contracts/contracts/project/pipelines/new/pipelines#new-post_create_a_new_pipeline.json (renamed from spec/contracts/contracts/project/pipeline/new/pipelines#new-post_create_a_new_pipeline.json)4
-rw-r--r--spec/contracts/contracts/project/pipelines/show/pipelines#show-delete_pipeline.json (renamed from spec/contracts/contracts/project/pipeline/show/pipelines#show-delete_pipeline.json)0
-rw-r--r--spec/contracts/contracts/project/pipelines/show/pipelines#show-get_pipeline_header_data.json (renamed from spec/contracts/contracts/project/pipeline/show/pipelines#show-get_pipeline_header_data.json)0
-rw-r--r--spec/contracts/provider/helpers/contract_source_helper.rb53
-rw-r--r--spec/contracts/provider/helpers/publish_contract_helper.rb12
-rw-r--r--spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_batch_helper.rb18
-rw-r--r--spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_metadata_helper.rb20
-rw-r--r--spec/contracts/provider/pact_helpers/project/merge_request/show/discussions_helper.rb20
-rw-r--r--spec/contracts/provider/pact_helpers/project/merge_requests/show/get_diffs_batch_helper.rb24
-rw-r--r--spec/contracts/provider/pact_helpers/project/merge_requests/show/get_diffs_metadata_helper.rb24
-rw-r--r--spec/contracts/provider/pact_helpers/project/merge_requests/show/get_discussions_helper.rb24
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipeline/index/create_a_new_pipeline_helper.rb20
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipeline/index/get_list_project_pipelines_helper.rb20
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipeline/show/delete_pipeline_helper.rb21
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipeline/show/get_pipeline_header_data_helper.rb22
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipeline_schedule/update_pipeline_schedule_helper.rb20
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipeline_schedules/edit/put_edit_a_pipeline_schedule_helper.rb24
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipelines/index/get_list_project_pipelines_helper.rb24
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipelines/new/post_create_a_new_pipeline_helper.rb24
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipelines/show/delete_pipeline_helper.rb24
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipelines/show/get_pipeline_header_data_helper.rb24
-rw-r--r--spec/contracts/provider/spec_helper.rb9
-rw-r--r--spec/contracts/provider/states/project/merge_requests/show_state.rb (renamed from spec/contracts/provider/states/project/merge_request/show_state.rb)2
-rw-r--r--spec/contracts/provider/states/project/pipeline_schedules/edit_state.rb (renamed from spec/contracts/provider/states/project/pipeline_schedule/edit_state.rb)0
-rw-r--r--spec/contracts/provider/states/project/pipelines/index_state.rb (renamed from spec/contracts/provider/states/project/pipeline/index_state.rb)0
-rw-r--r--spec/contracts/provider/states/project/pipelines/new_state.rb (renamed from spec/contracts/provider/states/project/pipeline/new_state.rb)0
-rw-r--r--spec/contracts/provider/states/project/pipelines/show_state.rb (renamed from spec/contracts/provider/states/project/pipeline/show_state.rb)0
-rw-r--r--spec/contracts/provider_specs/helpers/provider/contract_source_helper_spec.rb96
-rw-r--r--spec/controllers/admin/application_settings/appearances_controller_spec.rb1
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb20
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb44
-rw-r--r--spec/controllers/admin/hooks_controller_spec.rb2
-rw-r--r--spec/controllers/admin/plan_limits_controller_spec.rb20
-rw-r--r--spec/controllers/admin/runner_projects_controller_spec.rb2
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb2
-rw-r--r--spec/controllers/concerns/check_rate_limit_spec.rb6
-rw-r--r--spec/controllers/concerns/issuable_actions_spec.rb6
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb35
-rw-r--r--spec/controllers/explore/projects_controller_spec.rb1
-rw-r--r--spec/controllers/graphql_controller_spec.rb19
-rw-r--r--spec/controllers/groups/application_controller_spec.rb45
-rw-r--r--spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb8
-rw-r--r--spec/controllers/groups/labels_controller_spec.rb2
-rw-r--r--spec/controllers/groups/registry/repositories_controller_spec.rb2
-rw-r--r--spec/controllers/groups/runners_controller_spec.rb2
-rw-r--r--spec/controllers/groups/settings/repository_controller_spec.rb2
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb24
-rw-r--r--spec/controllers/import/github_controller_spec.rb274
-rw-r--r--spec/controllers/jira_connect/events_controller_spec.rb38
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb26
-rw-r--r--spec/controllers/profiles/keys_controller_spec.rb3
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb28
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb44
-rw-r--r--spec/controllers/projects/hooks_controller_spec.rb24
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb38
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/conflicts_controller_spec.rb14
-rw-r--r--spec/controllers/projects/merge_requests/creations_controller_spec.rb14
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb3
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb8
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb80
-rw-r--r--spec/controllers/projects/registry/repositories_controller_spec.rb16
-rw-r--r--spec/controllers/projects/runners_controller_spec.rb2
-rw-r--r--spec/controllers/projects/service_ping_controller_spec.rb9
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb53
-rw-r--r--spec/controllers/projects/settings/integrations_controller_spec.rb222
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb38
-rw-r--r--spec/controllers/projects_controller_spec.rb53
-rw-r--r--spec/controllers/registrations_controller_spec.rb130
-rw-r--r--spec/controllers/search_controller_spec.rb6
-rw-r--r--spec/controllers/sessions_controller_spec.rb16
-rw-r--r--spec/db/development/create_work_item_hierarchy_restrictions_spec.rb9
-rw-r--r--spec/db/docs_spec.rb101
-rw-r--r--spec/db/migration_spec.rb1
-rw-r--r--spec/db/production/create_work_item_hierarchy_restrictions_spec.rb9
-rw-r--r--spec/db/schema_spec.rb127
-rw-r--r--spec/factories/achievements/achievements.rb9
-rw-r--r--spec/factories/bulk_import.rb1
-rw-r--r--spec/factories/bulk_import/trackers.rb9
-rw-r--r--spec/factories/ci/build_runner_sessions.rb8
-rw-r--r--spec/factories/ci/builds.rb8
-rw-r--r--spec/factories/ci/pipelines.rb4
-rw-r--r--spec/factories/ci/resource.rb1
-rw-r--r--spec/factories/ci/sources/pipelines.rb4
-rw-r--r--spec/factories/ci/unit_test_failures.rb (renamed from spec/factories/ci/unit_test_failure.rb)0
-rw-r--r--spec/factories/ci/unit_tests.rb (renamed from spec/factories/ci/unit_test.rb)0
-rw-r--r--spec/factories/clusters/agents/group_authorizations.rb10
-rw-r--r--spec/factories/clusters/agents/project_authorizations.rb10
-rw-r--r--spec/factories/dependency_proxy.rb9
-rw-r--r--spec/factories/deploy_tokens.rb2
-rw-r--r--spec/factories/events.rb5
-rw-r--r--spec/factories/groups.rb4
-rw-r--r--spec/factories/issues.rb11
-rw-r--r--spec/factories/ml/candidate_metadata.rb10
-rw-r--r--spec/factories/ml/candidates.rb6
-rw-r--r--spec/factories/ml/experiment_metadata.rb10
-rw-r--r--spec/factories/ml/experiments.rb8
-rw-r--r--spec/factories/packages/rpm/rpm_repository_files.rb4
-rw-r--r--spec/factories/project_export_jobs.rb16
-rw-r--r--spec/factories/projects/ci_feature_usages.rb1
-rw-r--r--spec/factories/projects/import_export/relation_export_upload.rb2
-rw-r--r--spec/factories/resource_milestone_events.rb (renamed from spec/factories/resource_milestone_event.rb)0
-rw-r--r--spec/factories/resource_state_events.rb (renamed from spec/factories/resource_state_event.rb)0
-rw-r--r--spec/factories/todos.rb4
-rw-r--r--spec/factories/work_items.rb10
-rw-r--r--spec/factories/work_items/hierarchy_restrictions.rb8
-rw-r--r--spec/factories/work_items/work_item_types.rb2
-rw-r--r--spec/features/abuse_report_spec.rb2
-rw-r--r--spec/features/action_cable_logging_spec.rb2
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb2
-rw-r--r--spec/features/admin/admin_appearance_spec.rb2
-rw-r--r--spec/features/admin/admin_broadcast_messages_spec.rb2
-rw-r--r--spec/features/admin/admin_browse_spam_logs_spec.rb9
-rw-r--r--spec/features/admin/admin_deploy_keys_spec.rb2
-rw-r--r--spec/features/admin/admin_dev_ops_reports_spec.rb2
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb2
-rw-r--r--spec/features/admin/admin_disables_two_factor_spec.rb2
-rw-r--r--spec/features/admin/admin_groups_spec.rb2
-rw-r--r--spec/features/admin/admin_health_check_spec.rb2
-rw-r--r--spec/features/admin/admin_hook_logs_spec.rb2
-rw-r--r--spec/features/admin/admin_hooks_spec.rb15
-rw-r--r--spec/features/admin/admin_jobs_spec.rb2
-rw-r--r--spec/features/admin/admin_labels_spec.rb2
-rw-r--r--spec/features/admin/admin_manage_applications_spec.rb2
-rw-r--r--spec/features/admin/admin_mode/login_spec.rb2
-rw-r--r--spec/features/admin/admin_mode/logout_spec.rb2
-rw-r--r--spec/features/admin/admin_mode/workers_spec.rb2
-rw-r--r--spec/features/admin/admin_mode_spec.rb2
-rw-r--r--spec/features/admin/admin_projects_spec.rb2
-rw-r--r--spec/features/admin/admin_runners_spec.rb18
-rw-r--r--spec/features/admin/admin_search_settings_spec.rb2
-rw-r--r--spec/features/admin/admin_sees_background_migrations_spec.rb2
-rw-r--r--spec/features/admin/admin_sees_project_statistics_spec.rb2
-rw-r--r--spec/features/admin/admin_sees_projects_statistics_spec.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb12
-rw-r--r--spec/features/admin/admin_system_info_spec.rb2
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb2
-rw-r--r--spec/features/admin/admin_users_spec.rb2
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb2
-rw-r--r--spec/features/admin/dashboard_spec.rb6
-rw-r--r--spec/features/admin/integrations/instance_integrations_spec.rb2
-rw-r--r--spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb3
-rw-r--r--spec/features/admin/users/user_spec.rb2
-rw-r--r--spec/features/admin/users/users_spec.rb6
-rw-r--r--spec/features/admin_variables_spec.rb2
-rw-r--r--spec/features/alert_management/alert_details_spec.rb6
-rw-r--r--spec/features/alert_management/alert_management_list_spec.rb2
-rw-r--r--spec/features/alert_management/user_filters_alerts_by_status_spec.rb2
-rw-r--r--spec/features/alert_management/user_searches_alerts_spec.rb2
-rw-r--r--spec/features/alert_management/user_updates_alert_status_spec.rb2
-rw-r--r--spec/features/alert_management_spec.rb2
-rw-r--r--spec/features/alerts_settings/user_views_alerts_settings_spec.rb2
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb2
-rw-r--r--spec/features/atom/dashboard_spec.rb2
-rw-r--r--spec/features/atom/issues_spec.rb2
-rw-r--r--spec/features/atom/merge_requests_spec.rb2
-rw-r--r--spec/features/atom/users_spec.rb2
-rw-r--r--spec/features/boards/board_filters_spec.rb4
-rw-r--r--spec/features/boards/boards_spec.rb124
-rw-r--r--spec/features/boards/focus_mode_spec.rb2
-rw-r--r--spec/features/boards/issue_ordering_spec.rb2
-rw-r--r--spec/features/boards/keyboard_shortcut_spec.rb2
-rw-r--r--spec/features/boards/multi_select_spec.rb2
-rw-r--r--spec/features/boards/multiple_boards_spec.rb2
-rw-r--r--spec/features/boards/new_issue_spec.rb2
-rw-r--r--spec/features/boards/reload_boards_on_browser_back_spec.rb2
-rw-r--r--spec/features/boards/sidebar_assignee_spec.rb5
-rw-r--r--spec/features/boards/sidebar_labels_in_namespaces_spec.rb2
-rw-r--r--spec/features/boards/sidebar_labels_spec.rb2
-rw-r--r--spec/features/boards/sidebar_spec.rb2
-rw-r--r--spec/features/boards/user_adds_lists_to_board_spec.rb2
-rw-r--r--spec/features/boards/user_visits_board_spec.rb2
-rw-r--r--spec/features/breadcrumbs_schema_markup_spec.rb2
-rw-r--r--spec/features/broadcast_messages_spec.rb8
-rw-r--r--spec/features/calendar_spec.rb19
-rw-r--r--spec/features/callouts/registration_enabled_spec.rb6
-rw-r--r--spec/features/canonical_link_spec.rb2
-rw-r--r--spec/features/clusters/cluster_detail_page_spec.rb2
-rw-r--r--spec/features/clusters/cluster_health_dashboard_spec.rb3
-rw-r--r--spec/features/clusters/create_agent_spec.rb6
-rw-r--r--spec/features/commit_spec.rb2
-rw-r--r--spec/features/commits/user_uses_quick_actions_spec.rb2
-rw-r--r--spec/features/commits/user_view_commits_spec.rb2
-rw-r--r--spec/features/commits_spec.rb4
-rw-r--r--spec/features/contextual_sidebar_spec.rb2
-rw-r--r--spec/features/cycle_analytics_spec.rb2
-rw-r--r--spec/features/dashboard/activity_spec.rb2
-rw-r--r--spec/features/dashboard/archived_projects_spec.rb2
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb2
-rw-r--r--spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb2
-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.rb25
-rw-r--r--spec/features/dashboard/issues_filter_spec.rb2
-rw-r--r--spec/features/dashboard/issues_spec.rb2
-rw-r--r--spec/features/dashboard/label_filter_spec.rb2
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb2
-rw-r--r--spec/features/dashboard/milestones_spec.rb2
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb2
-rw-r--r--spec/features/dashboard/projects_spec.rb10
-rw-r--r--spec/features/dashboard/root_explore_spec.rb24
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb2
-rw-r--r--spec/features/dashboard/snippets_spec.rb2
-rw-r--r--spec/features/dashboard/todos/target_state_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_filtering_spec.rb22
-rw-r--r--spec/features/dashboard/todos/todos_sorting_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb119
-rw-r--r--spec/features/dashboard/user_filters_projects_spec.rb177
-rw-r--r--spec/features/discussion_comments/commit_spec.rb2
-rw-r--r--spec/features/discussion_comments/issue_spec.rb2
-rw-r--r--spec/features/discussion_comments/merge_request_spec.rb2
-rw-r--r--spec/features/discussion_comments/snippets_spec.rb2
-rw-r--r--spec/features/display_system_header_and_footer_bar_spec.rb2
-rw-r--r--spec/features/error_pages_spec.rb2
-rw-r--r--spec/features/error_tracking/user_filters_errors_by_status_spec.rb3
-rw-r--r--spec/features/error_tracking/user_searches_sentry_errors_spec.rb3
-rw-r--r--spec/features/error_tracking/user_sees_error_details_spec.rb3
-rw-r--r--spec/features/error_tracking/user_sees_error_index_spec.rb3
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb2
-rw-r--r--spec/features/explore/groups_list_spec.rb2
-rw-r--r--spec/features/explore/groups_spec.rb2
-rw-r--r--spec/features/explore/topics_spec.rb2
-rw-r--r--spec/features/explore/user_explores_projects_spec.rb4
-rw-r--r--spec/features/file_uploads/attachment_spec.rb2
-rw-r--r--spec/features/file_uploads/ci_artifact_spec.rb2
-rw-r--r--spec/features/file_uploads/git_lfs_spec.rb2
-rw-r--r--spec/features/file_uploads/graphql_add_design_spec.rb2
-rw-r--r--spec/features/file_uploads/group_import_spec.rb2
-rw-r--r--spec/features/file_uploads/maven_package_spec.rb2
-rw-r--r--spec/features/file_uploads/multipart_invalid_uploads_spec.rb4
-rw-r--r--spec/features/file_uploads/nuget_package_spec.rb2
-rw-r--r--spec/features/file_uploads/project_import_spec.rb2
-rw-r--r--spec/features/file_uploads/rubygem_package_spec.rb2
-rw-r--r--spec/features/file_uploads/user_avatar_spec.rb2
-rw-r--r--spec/features/frequently_visited_projects_and_groups_spec.rb2
-rw-r--r--spec/features/gitlab_experiments_spec.rb2
-rw-r--r--spec/features/global_search_spec.rb2
-rw-r--r--spec/features/graphiql_spec.rb2
-rw-r--r--spec/features/graphql_known_operations_spec.rb2
-rw-r--r--spec/features/group_variables_spec.rb2
-rw-r--r--spec/features/groups/activity_spec.rb2
-rw-r--r--spec/features/groups/board_sidebar_spec.rb2
-rw-r--r--spec/features/groups/board_spec.rb4
-rw-r--r--spec/features/groups/clusters/user_spec.rb2
-rw-r--r--spec/features/groups/container_registry_spec.rb4
-rw-r--r--spec/features/groups/crm/contacts/create_spec.rb2
-rw-r--r--spec/features/groups/dependency_proxy_for_containers_spec.rb2
-rw-r--r--spec/features/groups/dependency_proxy_spec.rb2
-rw-r--r--spec/features/groups/empty_states_spec.rb2
-rw-r--r--spec/features/groups/group_page_with_external_authorization_service_spec.rb2
-rw-r--r--spec/features/groups/group_runners_spec.rb2
-rw-r--r--spec/features/groups/group_settings_spec.rb4
-rw-r--r--spec/features/groups/import_export/connect_instance_spec.rb108
-rw-r--r--spec/features/groups/import_export/export_file_spec.rb2
-rw-r--r--spec/features/groups/import_export/import_file_spec.rb2
-rw-r--r--spec/features/groups/import_export/migration_history_spec.rb2
-rw-r--r--spec/features/groups/integrations/user_activates_mattermost_slash_command_spec.rb2
-rw-r--r--spec/features/groups/issues_spec.rb6
-rw-r--r--spec/features/groups/labels/create_spec.rb2
-rw-r--r--spec/features/groups/labels/edit_spec.rb2
-rw-r--r--spec/features/groups/labels/index_spec.rb2
-rw-r--r--spec/features/groups/labels/search_labels_spec.rb2
-rw-r--r--spec/features/groups/labels/sort_labels_spec.rb2
-rw-r--r--spec/features/groups/labels/subscription_spec.rb2
-rw-r--r--spec/features/groups/labels/user_sees_links_to_issuables_spec.rb2
-rw-r--r--spec/features/groups/members/filter_members_spec.rb2
-rw-r--r--spec/features/groups/members/leave_group_spec.rb2
-rw-r--r--spec/features/groups/members/list_members_spec.rb2
-rw-r--r--spec/features/groups/members/manage_groups_spec.rb2
-rw-r--r--spec/features/groups/members/manage_members_spec.rb4
-rw-r--r--spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb2
-rw-r--r--spec/features/groups/members/master_manages_access_requests_spec.rb2
-rw-r--r--spec/features/groups/members/request_access_spec.rb2
-rw-r--r--spec/features/groups/members/search_members_spec.rb2
-rw-r--r--spec/features/groups/members/sort_members_spec.rb2
-rw-r--r--spec/features/groups/members/tabs_spec.rb2
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/groups/milestone_spec.rb2
-rw-r--r--spec/features/groups/milestones/gfm_autocomplete_spec.rb2
-rw-r--r--spec/features/groups/milestones_sorting_spec.rb2
-rw-r--r--spec/features/groups/navbar_spec.rb2
-rw-r--r--spec/features/groups/new_group_page_spec.rb2
-rw-r--r--spec/features/groups/packages_spec.rb2
-rw-r--r--spec/features/groups/settings/access_tokens_spec.rb2
-rw-r--r--spec/features/groups/settings/ci_cd_spec.rb7
-rw-r--r--spec/features/groups/settings/group_badges_spec.rb2
-rw-r--r--spec/features/groups/settings/manage_applications_spec.rb2
-rw-r--r--spec/features/groups/settings/packages_and_registries_spec.rb2
-rw-r--r--spec/features/groups/settings/repository_spec.rb2
-rw-r--r--spec/features/groups/settings/user_searches_in_settings_spec.rb2
-rw-r--r--spec/features/groups/share_lock_spec.rb2
-rw-r--r--spec/features/groups/show_spec.rb2
-rw-r--r--spec/features/groups/user_browse_projects_group_page_spec.rb2
-rw-r--r--spec/features/groups/user_sees_package_sidebar_spec.rb2
-rw-r--r--spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb3
-rw-r--r--spec/features/groups_spec.rb10
-rw-r--r--spec/features/help_dropdown_spec.rb2
-rw-r--r--spec/features/help_pages_spec.rb2
-rw-r--r--spec/features/ics/dashboard_issues_spec.rb2
-rw-r--r--spec/features/ics/group_issues_spec.rb2
-rw-r--r--spec/features/ics/project_issues_spec.rb2
-rw-r--r--spec/features/ide/clientside_preview_csp_spec.rb2
-rw-r--r--spec/features/ide/static_object_external_storage_csp_spec.rb2
-rw-r--r--spec/features/ide/user_opens_merge_request_spec.rb2
-rw-r--r--spec/features/ide_spec.rb19
-rw-r--r--spec/features/import/manifest_import_spec.rb4
-rw-r--r--spec/features/incidents/incident_details_spec.rb2
-rw-r--r--spec/features/incidents/incident_timeline_events_spec.rb10
-rw-r--r--spec/features/incidents/incidents_list_spec.rb2
-rw-r--r--spec/features/incidents/user_creates_new_incident_spec.rb2
-rw-r--r--spec/features/incidents/user_filters_incidents_by_status_spec.rb2
-rw-r--r--spec/features/incidents/user_searches_incidents_spec.rb2
-rw-r--r--spec/features/incidents/user_uses_quick_actions_spec.rb2
-rw-r--r--spec/features/incidents/user_views_incident_spec.rb4
-rw-r--r--spec/features/invites_spec.rb10
-rw-r--r--spec/features/issuables/issuable_list_spec.rb8
-rw-r--r--spec/features/issuables/markdown_references/internal_references_spec.rb2
-rw-r--r--spec/features/issuables/markdown_references/jira_spec.rb2
-rw-r--r--spec/features/issuables/shortcuts_issuable_spec.rb4
-rw-r--r--spec/features/issuables/sorting_list_spec.rb2
-rw-r--r--spec/features/issuables/user_sees_sidebar_spec.rb2
-rw-r--r--spec/features/issue_rebalancing_spec.rb6
-rw-r--r--spec/features/issues/confidential_notes_spec.rb2
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb2
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb2
-rw-r--r--spec/features/issues/csv_spec.rb2
-rw-r--r--spec/features/issues/discussion_lock_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_base_spec.rb3
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb3
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb3
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb3
-rw-r--r--spec/features/issues/filtered_search/dropdown_release_spec.rb3
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/recent_searches_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb2
-rw-r--r--spec/features/issues/form_spec.rb4
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb2
-rw-r--r--spec/features/issues/group_label_sidebar_spec.rb2
-rw-r--r--spec/features/issues/incident_issue_spec.rb2
-rw-r--r--spec/features/issues/issue_detail_spec.rb2
-rw-r--r--spec/features/issues/issue_header_spec.rb10
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb2
-rw-r--r--spec/features/issues/issue_state_spec.rb2
-rw-r--r--spec/features/issues/keyboard_shortcut_spec.rb2
-rw-r--r--spec/features/issues/markdown_toolbar_spec.rb2
-rw-r--r--spec/features/issues/move_spec.rb2
-rw-r--r--spec/features/issues/note_polling_spec.rb2
-rw-r--r--spec/features/issues/notes_on_issues_spec.rb2
-rw-r--r--spec/features/issues/related_issues_spec.rb2
-rw-r--r--spec/features/issues/resource_label_events_spec.rb2
-rw-r--r--spec/features/issues/rss_spec.rb2
-rw-r--r--spec/features/issues/service_desk_spec.rb2
-rw-r--r--spec/features/issues/spam_akismet_issue_creation_spec.rb2
-rw-r--r--spec/features/issues/todo_spec.rb2
-rw-r--r--spec/features/issues/user_bulk_edits_issues_labels_spec.rb2
-rw-r--r--spec/features/issues/user_bulk_edits_issues_spec.rb2
-rw-r--r--spec/features/issues/user_comments_on_issue_spec.rb2
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb6
-rw-r--r--spec/features/issues/user_creates_confidential_merge_request_spec.rb2
-rw-r--r--spec/features/issues/user_creates_issue_by_email_spec.rb2
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb2
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb4
-rw-r--r--spec/features/issues/user_filters_issues_spec.rb2
-rw-r--r--spec/features/issues/user_interacts_with_awards_spec.rb6
-rw-r--r--spec/features/issues/user_resets_their_incoming_email_token_spec.rb2
-rw-r--r--spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb2
-rw-r--r--spec/features/issues/user_sees_breadcrumb_links_spec.rb2
-rw-r--r--spec/features/issues/user_sees_empty_state_spec.rb2
-rw-r--r--spec/features/issues/user_sees_live_update_spec.rb2
-rw-r--r--spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb2
-rw-r--r--spec/features/issues/user_sorts_issue_comments_spec.rb2
-rw-r--r--spec/features/issues/user_sorts_issues_spec.rb2
-rw-r--r--spec/features/issues/user_toggles_subscription_spec.rb2
-rw-r--r--spec/features/issues/user_uses_quick_actions_spec.rb2
-rw-r--r--spec/features/issues/user_views_issue_spec.rb2
-rw-r--r--spec/features/issues/user_views_issues_spec.rb2
-rw-r--r--spec/features/jira_connect/branches_spec.rb24
-rw-r--r--spec/features/jira_connect/subscriptions_spec.rb2
-rw-r--r--spec/features/jira_oauth_provider_authorize_spec.rb2
-rw-r--r--spec/features/labels_hierarchy_spec.rb3
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb12
-rw-r--r--spec/features/markdown/gitlab_flavored_markdown_spec.rb2
-rw-r--r--spec/features/markdown/json_table_spec.rb2
-rw-r--r--spec/features/markdown/keyboard_shortcuts_spec.rb2
-rw-r--r--spec/features/markdown/kroki_spec.rb2
-rw-r--r--spec/features/markdown/markdown_spec.rb9
-rw-r--r--spec/features/markdown/math_spec.rb2
-rw-r--r--spec/features/markdown/metrics_spec.rb2
-rw-r--r--spec/features/markdown/observability_spec.rb83
-rw-r--r--spec/features/markdown/sandboxed_mermaid_spec.rb2
-rw-r--r--spec/features/merge_request/batch_comments_spec.rb2
-rw-r--r--spec/features/merge_request/close_reopen_report_toggle_spec.rb20
-rw-r--r--spec/features/merge_request/maintainer_edits_fork_spec.rb3
-rw-r--r--spec/features/merge_request/merge_request_discussion_lock_spec.rb2
-rw-r--r--spec/features/merge_request/user_accepts_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb3
-rw-r--r--spec/features/merge_request/user_approves_spec.rb2
-rw-r--r--spec/features/merge_request/user_assigns_themselves_reviewer_spec.rb42
-rw-r--r--spec/features/merge_request/user_assigns_themselves_spec.rb16
-rw-r--r--spec/features/merge_request/user_awards_emoji_spec.rb2
-rw-r--r--spec/features/merge_request/user_clicks_merge_request_tabs_spec.rb2
-rw-r--r--spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb3
-rw-r--r--spec/features/merge_request/user_comments_on_commit_spec.rb2
-rw-r--r--spec/features/merge_request/user_comments_on_diff_spec.rb2
-rw-r--r--spec/features/merge_request/user_comments_on_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_creates_merge_request_spec.rb4
-rw-r--r--spec/features/merge_request/user_creates_mr_spec.rb2
-rw-r--r--spec/features/merge_request/user_customizes_merge_commit_message_spec.rb2
-rw-r--r--spec/features/merge_request/user_edits_assignees_sidebar_spec.rb2
-rw-r--r--spec/features/merge_request/user_edits_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_edits_mr_spec.rb2
-rw-r--r--spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb2
-rw-r--r--spec/features/merge_request/user_expands_diff_spec.rb2
-rw-r--r--spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb2
-rw-r--r--spec/features/merge_request/user_locks_discussion_spec.rb2
-rw-r--r--spec/features/merge_request/user_manages_subscription_spec.rb2
-rw-r--r--spec/features/merge_request/user_marks_merge_request_as_draft_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.rb2
-rw-r--r--spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb2
-rw-r--r--spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb2
-rw-r--r--spec/features/merge_request/user_opens_context_commits_modal_spec.rb2
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_rebases_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb2
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb2
-rw-r--r--spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb2
-rw-r--r--spec/features/merge_request/user_resolves_wip_mr_spec.rb2
-rw-r--r--spec/features/merge_request/user_reverts_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_reviews_image_spec.rb2
-rw-r--r--spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_breadcrumb_links_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_closing_issues_message_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_deleted_target_branch_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_deployment_widget_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_diff_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_discussions_navigation_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_discussions_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb3
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_page_metadata_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb3
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb4
-rw-r--r--spec/features/merge_request/user_sees_suggest_pipeline_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_system_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_wip_help_message_spec.rb2
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb2
-rw-r--r--spec/features/merge_request/user_squashes_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb2
-rw-r--r--spec/features/merge_request/user_toggles_whitespace_changes_spec.rb2
-rw-r--r--spec/features/merge_request/user_tries_to_access_private_project_info_through_new_mr_spec.rb3
-rw-r--r--spec/features/merge_request/user_uses_quick_actions_spec.rb3
-rw-r--r--spec/features/merge_request/user_views_auto_expanding_diff_spec.rb2
-rw-r--r--spec/features/merge_request/user_views_diffs_commit_spec.rb2
-rw-r--r--spec/features/merge_request/user_views_diffs_file_by_file_spec.rb2
-rw-r--r--spec/features/merge_request/user_views_diffs_spec.rb2
-rw-r--r--spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb2
-rw-r--r--spec/features/merge_request/user_views_open_merge_request_spec.rb2
-rw-r--r--spec/features/merge_requests/filters_generic_behavior_spec.rb2
-rw-r--r--spec/features/merge_requests/rss_spec.rb2
-rw-r--r--spec/features/merge_requests/user_exports_as_csv_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_approvals_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_assignees_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_deployments_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_draft_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_labels_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_milestones_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_target_branch_spec.rb2
-rw-r--r--spec/features/merge_requests/user_lists_merge_requests_spec.rb2
-rw-r--r--spec/features/merge_requests/user_mass_updates_spec.rb2
-rw-r--r--spec/features/merge_requests/user_sees_empty_state_spec.rb2
-rw-r--r--spec/features/merge_requests/user_sorts_merge_requests_spec.rb2
-rw-r--r--spec/features/merge_requests/user_views_all_merge_requests_spec.rb2
-rw-r--r--spec/features/merge_requests/user_views_closed_merge_requests_spec.rb2
-rw-r--r--spec/features/merge_requests/user_views_merged_merge_requests_spec.rb2
-rw-r--r--spec/features/merge_requests/user_views_open_merge_requests_spec.rb2
-rw-r--r--spec/features/milestone_spec.rb2
-rw-r--r--spec/features/milestones/user_creates_milestone_spec.rb2
-rw-r--r--spec/features/milestones/user_deletes_milestone_spec.rb2
-rw-r--r--spec/features/milestones/user_edits_milestone_spec.rb2
-rw-r--r--spec/features/milestones/user_promotes_milestone_spec.rb2
-rw-r--r--spec/features/milestones/user_sees_breadcrumb_links_spec.rb2
-rw-r--r--spec/features/milestones/user_views_milestone_spec.rb2
-rw-r--r--spec/features/milestones/user_views_milestones_spec.rb2
-rw-r--r--spec/features/monitor_sidebar_link_spec.rb55
-rw-r--r--spec/features/nav/new_nav_toggle_spec.rb71
-rw-r--r--spec/features/nav/top_nav_responsive_spec.rb2
-rw-r--r--spec/features/nav/top_nav_tooltip_spec.rb2
-rw-r--r--spec/features/oauth_login_spec.rb2
-rw-r--r--spec/features/oauth_provider_authorize_spec.rb2
-rw-r--r--spec/features/oauth_registration_spec.rb2
-rw-r--r--spec/features/one_trust_spec.rb2
-rw-r--r--spec/features/participants_autocomplete_spec.rb2
-rw-r--r--spec/features/password_reset_spec.rb2
-rw-r--r--spec/features/populate_new_pipeline_vars_with_params_spec.rb44
-rw-r--r--spec/features/profile_spec.rb2
-rw-r--r--spec/features/profiles/account_spec.rb2
-rw-r--r--spec/features/profiles/active_sessions_spec.rb2
-rw-r--r--spec/features/profiles/chat_names_spec.rb2
-rw-r--r--spec/features/profiles/emails_spec.rb2
-rw-r--r--spec/features/profiles/gpg_keys_spec.rb2
-rw-r--r--spec/features/profiles/keys_spec.rb4
-rw-r--r--spec/features/profiles/oauth_applications_spec.rb2
-rw-r--r--spec/features/profiles/password_spec.rb2
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb2
-rw-r--r--spec/features/profiles/two_factor_auths_spec.rb2
-rw-r--r--spec/features/profiles/user_changes_notified_of_own_activity_spec.rb3
-rw-r--r--spec/features/profiles/user_edit_preferences_spec.rb22
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb2
-rw-r--r--spec/features/profiles/user_manages_applications_spec.rb2
-rw-r--r--spec/features/profiles/user_manages_emails_spec.rb2
-rw-r--r--spec/features/profiles/user_search_settings_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_notifications_tab_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_profile_account_page_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_profile_authentication_log_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb2
-rw-r--r--spec/features/project_group_variables_spec.rb2
-rw-r--r--spec/features/project_variables_spec.rb2
-rw-r--r--spec/features/projects/active_tabs_spec.rb2
-rw-r--r--spec/features/projects/activity/rss_spec.rb2
-rw-r--r--spec/features/projects/activity/user_sees_activity_spec.rb2
-rw-r--r--spec/features/projects/activity/user_sees_design_activity_spec.rb2
-rw-r--r--spec/features/projects/activity/user_sees_design_comment_spec.rb2
-rw-r--r--spec/features/projects/activity/user_sees_private_activity_spec.rb2
-rw-r--r--spec/features/projects/artifacts/file_spec.rb2
-rw-r--r--spec/features/projects/artifacts/raw_spec.rb2
-rw-r--r--spec/features/projects/artifacts/user_browses_artifacts_spec.rb2
-rw-r--r--spec/features/projects/artifacts/user_downloads_artifacts_spec.rb2
-rw-r--r--spec/features/projects/badges/coverage_spec.rb2
-rw-r--r--spec/features/projects/badges/list_spec.rb2
-rw-r--r--spec/features/projects/badges/pipeline_badge_spec.rb2
-rw-r--r--spec/features/projects/blobs/blame_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_line_permalink_updater_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb83
-rw-r--r--spec/features/projects/blobs/edit_spec.rb11
-rw-r--r--spec/features/projects/blobs/shortcuts_blob_spec.rb2
-rw-r--r--spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb2
-rw-r--r--spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb2
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/branches/new_branch_ref_dropdown_spec.rb71
-rw-r--r--spec/features/projects/branches/user_creates_branch_spec.rb8
-rw-r--r--spec/features/projects/branches/user_deletes_branch_spec.rb2
-rw-r--r--spec/features/projects/branches/user_views_branches_spec.rb2
-rw-r--r--spec/features/projects/branches_spec.rb6
-rw-r--r--spec/features/projects/ci/editor_spec.rb2
-rw-r--r--spec/features/projects/ci/lint_spec.rb2
-rw-r--r--spec/features/projects/classification_label_on_project_pages_spec.rb2
-rw-r--r--spec/features/projects/cluster_agents_spec.rb2
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb2
-rw-r--r--spec/features/projects/clusters/user_spec.rb2
-rw-r--r--spec/features/projects/clusters_spec.rb2
-rw-r--r--spec/features/projects/commit/builds_spec.rb2
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb2
-rw-r--r--spec/features/projects/commit/comments/user_adds_comment_spec.rb2
-rw-r--r--spec/features/projects/commit/comments/user_deletes_comments_spec.rb2
-rw-r--r--spec/features/projects/commit/comments/user_edits_comments_spec.rb2
-rw-r--r--spec/features/projects/commit/diff_notes_spec.rb2
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb2
-rw-r--r--spec/features/projects/commit/user_comments_on_commit_spec.rb2
-rw-r--r--spec/features/projects/commit/user_reverts_commit_spec.rb2
-rw-r--r--spec/features/projects/commit/user_views_user_status_on_commit_spec.rb2
-rw-r--r--spec/features/projects/commits/multi_view_diff_spec.rb2
-rw-r--r--spec/features/projects/commits/rss_spec.rb2
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb23
-rw-r--r--spec/features/projects/compare_spec.rb16
-rw-r--r--spec/features/projects/confluence/user_views_confluence_page_spec.rb2
-rw-r--r--spec/features/projects/container_registry_spec.rb4
-rw-r--r--spec/features/projects/deploy_keys_spec.rb2
-rw-r--r--spec/features/projects/diffs/diff_show_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_metrics_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb73
-rw-r--r--spec/features/projects/environments/environments_spec.rb2
-rw-r--r--spec/features/projects/feature_flag_user_lists/user_deletes_feature_flag_user_list_spec.rb2
-rw-r--r--spec/features/projects/feature_flag_user_lists/user_edits_feature_flag_user_list_spec.rb2
-rw-r--r--spec/features/projects/feature_flag_user_lists/user_sees_feature_flag_user_list_details_spec.rb2
-rw-r--r--spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb2
-rw-r--r--spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb6
-rw-r--r--spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb2
-rw-r--r--spec/features/projects/features_visibility_spec.rb2
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/files/edit_file_soft_wrap_spec.rb2
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb2
-rw-r--r--spec/features/projects/files/files_sort_submodules_with_folders_spec.rb2
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb2
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb3
-rw-r--r--spec/features/projects/files/template_selector_menu_spec.rb2
-rw-r--r--spec/features/projects/files/template_type_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/undo_template_spec.rb2
-rw-r--r--spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb3
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb39
-rw-r--r--spec/features/projects/files/user_browses_lfs_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_creates_directory_spec.rb2
-rw-r--r--spec/features/projects/files/user_creates_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_deletes_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_find_file_spec.rb2
-rw-r--r--spec/features/projects/files/user_reads_pipeline_status_spec.rb2
-rw-r--r--spec/features/projects/files/user_replaces_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_searches_for_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_uploads_files_spec.rb2
-rw-r--r--spec/features/projects/fork_spec.rb2
-rw-r--r--spec/features/projects/forks/fork_list_spec.rb2
-rw-r--r--spec/features/projects/gfm_autocomplete_load_spec.rb2
-rw-r--r--spec/features/projects/graph_spec.rb2
-rw-r--r--spec/features/projects/hook_logs/user_reads_log_spec.rb2
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb6
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/infrastructure_registry_spec.rb2
-rw-r--r--spec/features/projects/integrations/disable_triggers_spec.rb2
-rw-r--r--spec/features/projects/integrations/project_integrations_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_asana_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_assembla_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_emails_on_push_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_flowdock_spec.rb22
-rw-r--r--spec/features/projects/integrations/user_activates_irker_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_jetbrains_teamcity_ci_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_jira_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_packagist_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_prometheus_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_pushover_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_slack_notifications_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_uses_inherited_settings_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_views_services_spec.rb2
-rw-r--r--spec/features/projects/issuable_templates_spec.rb2
-rw-r--r--spec/features/projects/issues/design_management/user_links_to_designs_in_issue_spec.rb2
-rw-r--r--spec/features/projects/issues/design_management/user_paginates_designs_spec.rb2
-rw-r--r--spec/features/projects/issues/design_management/user_permissions_upload_spec.rb2
-rw-r--r--spec/features/projects/issues/design_management/user_uploads_designs_spec.rb2
-rw-r--r--spec/features/projects/issues/design_management/user_views_design_images_spec.rb2
-rw-r--r--spec/features/projects/issues/design_management/user_views_design_spec.rb2
-rw-r--r--spec/features/projects/issues/design_management/user_views_designs_spec.rb2
-rw-r--r--spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb2
-rw-r--r--spec/features/projects/issues/email_participants_spec.rb50
-rw-r--r--spec/features/projects/issues/viewing_relocated_issues_spec.rb2
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb2
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb2
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb2
-rw-r--r--spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb6
-rw-r--r--spec/features/projects/labels/issues_sorted_by_priority_spec.rb2
-rw-r--r--spec/features/projects/labels/search_labels_spec.rb2
-rw-r--r--spec/features/projects/labels/sort_labels_spec.rb2
-rw-r--r--spec/features/projects/labels/subscription_spec.rb2
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb2
-rw-r--r--spec/features/projects/labels/user_creates_labels_spec.rb2
-rw-r--r--spec/features/projects/labels/user_edits_labels_spec.rb2
-rw-r--r--spec/features/projects/labels/user_promotes_label_spec.rb2
-rw-r--r--spec/features/projects/labels/user_removes_labels_spec.rb2
-rw-r--r--spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb2
-rw-r--r--spec/features/projects/labels/user_sees_links_to_issuables_spec.rb2
-rw-r--r--spec/features/projects/labels/user_views_labels_spec.rb2
-rw-r--r--spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb2
-rw-r--r--spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb3
-rw-r--r--spec/features/projects/members/group_members_spec.rb2
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb3
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb2
-rw-r--r--spec/features/projects/members/manage_groups_spec.rb2
-rw-r--r--spec/features/projects/members/manage_members_spec.rb20
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb2
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb2
-rw-r--r--spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb2
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb2
-rw-r--r--spec/features/projects/members/owner_cannot_leave_project_spec.rb2
-rw-r--r--spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb2
-rw-r--r--spec/features/projects/members/sorting_spec.rb2
-rw-r--r--spec/features/projects/members/tabs_spec.rb2
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/merge_request_button_spec.rb2
-rw-r--r--spec/features/projects/milestones/gfm_autocomplete_spec.rb2
-rw-r--r--spec/features/projects/milestones/milestone_spec.rb2
-rw-r--r--spec/features/projects/milestones/milestones_sorting_spec.rb2
-rw-r--r--spec/features/projects/milestones/new_spec.rb2
-rw-r--r--spec/features/projects/milestones/user_interacts_with_labels_spec.rb2
-rw-r--r--spec/features/projects/navbar_spec.rb2
-rw-r--r--spec/features/projects/network_graph_spec.rb2
-rw-r--r--spec/features/projects/new_project_from_template_spec.rb2
-rw-r--r--spec/features/projects/new_project_spec.rb2
-rw-r--r--spec/features/projects/package_files_spec.rb2
-rw-r--r--spec/features/projects/packages_spec.rb2
-rw-r--r--spec/features/projects/pages/user_adds_domain_spec.rb2
-rw-r--r--spec/features/projects/pages/user_configures_pages_pipeline_spec.rb10
-rw-r--r--spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb2
-rw-r--r--spec/features/projects/pages/user_edits_settings_spec.rb2
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb6
-rw-r--r--spec/features/projects/pipelines/legacy_pipeline_spec.rb1315
-rw-r--r--spec/features/projects/pipelines/legacy_pipelines_spec.rb852
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb74
-rw-r--r--spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb2
-rw-r--r--spec/features/projects/releases/user_creates_release_spec.rb2
-rw-r--r--spec/features/projects/releases/user_views_edit_release_spec.rb2
-rw-r--r--spec/features/projects/releases/user_views_release_spec.rb2
-rw-r--r--spec/features/projects/releases/user_views_releases_spec.rb2
-rw-r--r--spec/features/projects/remote_mirror_spec.rb2
-rw-r--r--spec/features/projects/settings/access_tokens_spec.rb2
-rw-r--r--spec/features/projects/settings/branch_names_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/branch_rules_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/external_authorization_service_settings_spec.rb3
-rw-r--r--spec/features/projects/settings/forked_project_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/lfs_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/monitor_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/packages_settings_spec.rb6
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb4
-rw-r--r--spec/features/projects/settings/project_badges_spec.rb2
-rw-r--r--spec/features/projects/settings/project_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb3
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb3
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb47
-rw-r--r--spec/features/projects/settings/secure_files_spec.rb29
-rw-r--r--spec/features/projects/settings/service_desk_setting_spec.rb2
-rw-r--r--spec/features/projects/settings/user_archives_project_spec.rb2
-rw-r--r--spec/features/projects/settings/user_changes_avatar_spec.rb2
-rw-r--r--spec/features/projects/settings/user_changes_default_branch_spec.rb2
-rw-r--r--spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb2
-rw-r--r--spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb4
-rw-r--r--spec/features/projects/settings/user_renames_a_project_spec.rb2
-rw-r--r--spec/features/projects/settings/user_searches_in_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb2
-rw-r--r--spec/features/projects/settings/user_tags_project_spec.rb2
-rw-r--r--spec/features/projects/settings/user_transfers_a_project_spec.rb2
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb22
-rw-r--r--spec/features/projects/settings/webhooks_settings_spec.rb60
-rw-r--r--spec/features/projects/show/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/show/no_password_spec.rb2
-rw-r--r--spec/features/projects/show/redirects_spec.rb2
-rw-r--r--spec/features/projects/show/rss_spec.rb2
-rw-r--r--spec/features/projects/show/schema_markup_spec.rb2
-rw-r--r--spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb3
-rw-r--r--spec/features/projects/show/user_interacts_with_stars_spec.rb3
-rw-r--r--spec/features/projects/show/user_manages_notifications_spec.rb5
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_deletion_failure_message_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_git_instructions_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_readme_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb2
-rw-r--r--spec/features/projects/show/user_uploads_files_spec.rb2
-rw-r--r--spec/features/projects/snippets/create_snippet_spec.rb2
-rw-r--r--spec/features/projects/snippets/show_spec.rb2
-rw-r--r--spec/features/projects/snippets/user_comments_on_snippet_spec.rb2
-rw-r--r--spec/features/projects/snippets/user_deletes_snippet_spec.rb2
-rw-r--r--spec/features/projects/snippets/user_updates_snippet_spec.rb2
-rw-r--r--spec/features/projects/snippets/user_views_snippets_spec.rb2
-rw-r--r--spec/features/projects/sourcegraph_csp_spec.rb2
-rw-r--r--spec/features/projects/sub_group_issuables_spec.rb2
-rw-r--r--spec/features/projects/tags/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/tags/user_edits_tags_spec.rb2
-rw-r--r--spec/features/projects/tags/user_views_tag_spec.rb2
-rw-r--r--spec/features/projects/tags/user_views_tags_spec.rb2
-rw-r--r--spec/features/projects/terraform_spec.rb2
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb2
-rw-r--r--spec/features/projects/tree/create_file_spec.rb2
-rw-r--r--spec/features/projects/tree/rss_spec.rb2
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb17
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb2
-rw-r--r--spec/features/projects/user_changes_project_visibility_spec.rb2
-rw-r--r--spec/features/projects/user_creates_project_spec.rb2
-rw-r--r--spec/features/projects/user_sees_sidebar_spec.rb4
-rw-r--r--spec/features/projects/user_sees_user_popover_spec.rb2
-rw-r--r--spec/features/projects/user_sorts_projects_spec.rb8
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb2
-rw-r--r--spec/features/projects/user_views_empty_project_spec.rb2
-rw-r--r--spec/features/projects/view_on_env_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_views_wiki_empty_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb2
-rw-r--r--spec/features/projects/wikis_spec.rb2
-rw-r--r--spec/features/projects_spec.rb8
-rw-r--r--spec/features/promotion_spec.rb2
-rw-r--r--spec/features/protected_branches_spec.rb2
-rw-r--r--spec/features/protected_tags_spec.rb2
-rw-r--r--spec/features/read_only_spec.rb6
-rw-r--r--spec/features/reportable_note/issue_spec.rb2
-rw-r--r--spec/features/reportable_note/merge_request_spec.rb2
-rw-r--r--spec/features/reportable_note/snippets_spec.rb2
-rw-r--r--spec/features/runners_spec.rb54
-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.rb3
-rw-r--r--spec/features/search/user_searches_for_projects_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_users_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb3
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb2
-rw-r--r--spec/features/search/user_uses_search_filters_spec.rb2
-rw-r--r--spec/features/security/admin_access_spec.rb2
-rw-r--r--spec/features/security/dashboard_access_spec.rb2
-rw-r--r--spec/features/security/group/internal_access_spec.rb12
-rw-r--r--spec/features/security/group/private_access_spec.rb14
-rw-r--r--spec/features/security/group/public_access_spec.rb12
-rw-r--r--spec/features/security/profile_access_spec.rb2
-rw-r--r--spec/features/security/project/internal_access_spec.rb2
-rw-r--r--spec/features/security/project/private_access_spec.rb2
-rw-r--r--spec/features/security/project/public_access_spec.rb2
-rw-r--r--spec/features/security/project/snippet/internal_access_spec.rb2
-rw-r--r--spec/features/security/project/snippet/private_access_spec.rb2
-rw-r--r--spec/features/security/project/snippet/public_access_spec.rb2
-rw-r--r--spec/features/sentry_js_spec.rb57
-rw-r--r--spec/features/signed_commits_spec.rb2
-rw-r--r--spec/features/snippets/embedded_snippet_spec.rb2
-rw-r--r--spec/features/snippets/explore_spec.rb2
-rw-r--r--spec/features/snippets/internal_snippet_spec.rb2
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb2
-rw-r--r--spec/features/snippets/private_snippets_spec.rb2
-rw-r--r--spec/features/snippets/public_snippets_spec.rb2
-rw-r--r--spec/features/snippets/search_snippets_spec.rb2
-rw-r--r--spec/features/snippets/show_spec.rb2
-rw-r--r--spec/features/snippets/spam_snippets_spec.rb3
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb2
-rw-r--r--spec/features/snippets/user_deletes_snippet_spec.rb2
-rw-r--r--spec/features/snippets/user_edits_snippet_spec.rb2
-rw-r--r--spec/features/snippets/user_snippets_spec.rb2
-rw-r--r--spec/features/snippets_spec.rb2
-rw-r--r--spec/features/tags/developer_creates_tag_spec.rb46
-rw-r--r--spec/features/tags/developer_deletes_tag_spec.rb2
-rw-r--r--spec/features/tags/developer_views_tags_spec.rb2
-rw-r--r--spec/features/tags/maintainer_deletes_protected_tag_spec.rb2
-rw-r--r--spec/features/task_lists_spec.rb2
-rw-r--r--spec/features/topic_show_spec.rb2
-rw-r--r--spec/features/triggers_spec.rb2
-rw-r--r--spec/features/u2f_spec.rb3
-rw-r--r--spec/features/unsubscribe_links_spec.rb2
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_group_spec.rb2
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_profile_spec.rb2
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb2
-rw-r--r--spec/features/usage_stats_consent_spec.rb2
-rw-r--r--spec/features/user_can_display_performance_bar_spec.rb2
-rw-r--r--spec/features/user_opens_link_to_comment_spec.rb2
-rw-r--r--spec/features/user_sees_revert_modal_spec.rb3
-rw-r--r--spec/features/user_sorts_things_spec.rb4
-rw-r--r--spec/features/users/active_sessions_spec.rb28
-rw-r--r--spec/features/users/add_email_to_existing_account_spec.rb2
-rw-r--r--spec/features/users/anonymous_sessions_spec.rb2
-rw-r--r--spec/features/users/bizible_csp_spec.rb2
-rw-r--r--spec/features/users/confirmation_spec.rb2
-rw-r--r--spec/features/users/email_verification_on_login_spec.rb10
-rw-r--r--spec/features/users/google_analytics_csp_spec.rb2
-rw-r--r--spec/features/users/login_spec.rb6
-rw-r--r--spec/features/users/logout_spec.rb2
-rw-r--r--spec/features/users/one_trust_csp_spec.rb2
-rw-r--r--spec/features/users/overview_spec.rb2
-rw-r--r--spec/features/users/password_spec.rb2
-rw-r--r--spec/features/users/rss_spec.rb2
-rw-r--r--spec/features/users/show_spec.rb2
-rw-r--r--spec/features/users/signup_spec.rb13
-rw-r--r--spec/features/users/snippets_spec.rb2
-rw-r--r--spec/features/users/terms_spec.rb2
-rw-r--r--spec/features/users/user_browses_projects_on_user_page_spec.rb2
-rw-r--r--spec/features/users/zuora_csp_spec.rb2
-rw-r--r--spec/features/webauthn_spec.rb2
-rw-r--r--spec/features/whats_new_spec.rb2
-rw-r--r--spec/features/work_items/work_item_children_spec.rb2
-rw-r--r--spec/features/work_items/work_item_spec.rb29
-rw-r--r--spec/finders/autocomplete/routes_finder_spec.rb40
-rw-r--r--spec/finders/branches_finder_spec.rb63
-rw-r--r--spec/finders/ci/auth_job_finder_spec.rb4
-rw-r--r--spec/finders/ci/freeze_periods_finder_spec.rb (renamed from spec/finders/freeze_periods_finder_spec.rb)2
-rw-r--r--spec/finders/ci/jobs_finder_spec.rb88
-rw-r--r--spec/finders/ci/pipelines_finder_spec.rb41
-rw-r--r--spec/finders/ci/runners_finder_spec.rb447
-rw-r--r--spec/finders/clusters/agent_tokens_finder_spec.rb53
-rw-r--r--spec/finders/environments/environments_finder_spec.rb24
-rw-r--r--spec/finders/issues_finder_spec.rb2
-rw-r--r--spec/finders/notes_finder_spec.rb10
-rw-r--r--spec/finders/personal_access_tokens_finder_spec.rb475
-rw-r--r--spec/finders/projects_finder_spec.rb17
-rw-r--r--spec/finders/tags_finder_spec.rb13
-rw-r--r--spec/finders/todos_finder_spec.rb4
-rw-r--r--spec/finders/users_finder_spec.rb52
-rw-r--r--spec/finders/work_items/work_items_finder_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/branch.json15
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json131
-rw-r--r--spec/fixtures/api/schemas/conflicts.json130
-rw-r--r--spec/fixtures/api/schemas/entities/discussion.json231
-rw-r--r--spec/fixtures/api/schemas/entities/issue.json163
-rw-r--r--spec/fixtures/api/schemas/entities/member.json125
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_metrics.json43
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_poll_widget.json223
-rw-r--r--spec/fixtures/api/schemas/entities/test_case.json57
-rw-r--r--spec/fixtures/api/schemas/entities/trigger.json13
-rw-r--r--spec/fixtures/api/schemas/environment.json138
-rw-r--r--spec/fixtures/api/schemas/group_link/group_link.json84
-rw-r--r--spec/fixtures/api/schemas/issue.json135
-rw-r--r--spec/fixtures/api/schemas/jira_connect/author.json29
-rw-r--r--spec/fixtures/api/schemas/jira_connect/branch.json37
-rw-r--r--spec/fixtures/api/schemas/jira_connect/commit.json58
-rw-r--r--spec/fixtures/api/schemas/jira_connect/file.json29
-rw-r--r--spec/fixtures/api/schemas/jira_connect/pull_request.json69
-rw-r--r--spec/fixtures/api/schemas/jira_connect/repository.json45
-rw-r--r--spec/fixtures/api/schemas/job/build_trace_line.json22
-rw-r--r--spec/fixtures/api/schemas/merge_request.json15
-rw-r--r--spec/fixtures/api/schemas/ml/get_experiment.json27
-rw-r--r--spec/fixtures/api/schemas/ml/list_experiments.json11
-rw-r--r--spec/fixtures/api/schemas/ml/run.json30
-rw-r--r--spec/fixtures/api/schemas/ml/update_run.json48
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedule.json153
-rw-r--r--spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json64
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/basic_environment.json34
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/basic_environments.json6
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/branch.json43
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/deploy_key.json48
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/deploy_token.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/environments.json7
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/feature_flag_scopes.json7
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/feature_flags.json7
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/integration.json76
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issue.json326
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issues.json7
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/labels/label_with_counts.json30
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/labels/project_label.json29
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/labels/project_label_with_counts.json16
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_request.json261
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_request_simple.json73
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/milestone.json89
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/milestone_with_stats.json105
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/notes.json139
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/packages/package.json7
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json61
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json63
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/project_hooks.json8
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/snippets.json111
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/system_hooks.json7
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/admin.json146
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/login.json35
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/public.json138
-rw-r--r--spec/fixtures/api/schemas/registry/repository.json35
-rw-r--r--spec/fixtures/api/schemas/release.json58
-rw-r--r--spec/fixtures/api/schemas/variable.json34
-rw-r--r--spec/fixtures/ce_sample_schema.json1
-rw-r--r--spec/fixtures/clusters/chain_certificates.pem136
-rw-r--r--spec/fixtures/clusters/intermediate_certificate.pem44
-rw-r--r--spec/fixtures/clusters/leaf_certificate.pem54
-rw-r--r--spec/fixtures/clusters/root_certificate.pem38
-rw-r--r--spec/fixtures/config/redis_cluster_format_host.yml29
-rw-r--r--spec/fixtures/gitlab/database/structure_example.sql14
-rw-r--r--spec/fixtures/gitlab/database/structure_example_cleaned.sql13
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_full_syntax.json19
-rw-r--r--spec/fixtures/markdown.md.erb4
-rw-r--r--spec/fixtures/markdown/markdown_golden_master_examples.yml10
-rw-r--r--spec/fixtures/pager_duty/webhook_incident_trigger.json284
-rw-r--r--spec/frontend/__helpers__/dom_events_helper.js8
-rw-r--r--spec/frontend/__helpers__/filtered_search_spec_helper.js4
-rw-r--r--spec/frontend/__helpers__/graphql_helpers.js14
-rw-r--r--spec/frontend/__helpers__/graphql_helpers_spec.js23
-rw-r--r--spec/frontend/__helpers__/graphql_transformer.js4
-rw-r--r--spec/frontend/__helpers__/jest_helpers.js22
-rw-r--r--spec/frontend/__helpers__/mock_window_location_helper.js15
-rw-r--r--spec/frontend/__helpers__/raw_transformer.js2
-rw-r--r--spec/frontend/__helpers__/set_timeout_promise_helper.js4
-rw-r--r--spec/frontend/__helpers__/web_worker_transformer.js10
-rw-r--r--spec/frontend/__helpers__/yaml_transformer.js2
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js2
-rw-r--r--spec/frontend/admin/background_migrations/components/database_listbox_spec.js10
-rw-r--r--spec/frontend/admin/broadcast_messages/components/datetime_picker_spec.js46
-rw-r--r--spec/frontend/admin/broadcast_messages/components/message_form_spec.js201
-rw-r--r--spec/frontend/admin/broadcast_messages/mock_data.js8
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_form_spec.js1
-rw-r--r--spec/frontend/admin/signup_restrictions/mock_data.js2
-rw-r--r--spec/frontend/admin/signup_restrictions/utils_spec.js1
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap2
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js4
-rw-r--r--spec/frontend/analytics/cycle_analytics/__snapshots__/total_time_spec.js.snap (renamed from spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap)0
-rw-r--r--spec/frontend/analytics/cycle_analytics/base_spec.js (renamed from spec/frontend/cycle_analytics/base_spec.js)12
-rw-r--r--spec/frontend/analytics/cycle_analytics/filter_bar_spec.js (renamed from spec/frontend/cycle_analytics/filter_bar_spec.js)22
-rw-r--r--spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js (renamed from spec/frontend/cycle_analytics/formatted_stage_count_spec.js)2
-rw-r--r--spec/frontend/analytics/cycle_analytics/mock_data.js (renamed from spec/frontend/cycle_analytics/mock_data.js)2
-rw-r--r--spec/frontend/analytics/cycle_analytics/path_navigation_spec.js (renamed from spec/frontend/cycle_analytics/path_navigation_spec.js)2
-rw-r--r--spec/frontend/analytics/cycle_analytics/stage_table_spec.js (renamed from spec/frontend/cycle_analytics/stage_table_spec.js)4
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/actions_spec.js (renamed from spec/frontend/cycle_analytics/store/actions_spec.js)4
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/getters_spec.js (renamed from spec/frontend/cycle_analytics/store/getters_spec.js)2
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/mutations_spec.js (renamed from spec/frontend/cycle_analytics/store/mutations_spec.js)6
-rw-r--r--spec/frontend/analytics/cycle_analytics/total_time_spec.js (renamed from spec/frontend/cycle_analytics/total_time_spec.js)2
-rw-r--r--spec/frontend/analytics/cycle_analytics/utils_spec.js (renamed from spec/frontend/cycle_analytics/utils_spec.js)2
-rw-r--r--spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js (renamed from spec/frontend/cycle_analytics/value_stream_filters_spec.js)4
-rw-r--r--spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js (renamed from spec/frontend/cycle_analytics/value_stream_metrics_spec.js)0
-rw-r--r--spec/frontend/api_spec.js20
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js8
-rw-r--r--spec/frontend/batch_comments/components/preview_item_spec.js3
-rw-r--r--spec/frontend/batch_comments/components/publish_dropdown_spec.js3
-rw-r--r--spec/frontend/behaviors/markdown/render_observability_spec.js38
-rw-r--r--spec/frontend/blob/openapi/index_spec.js2
-rw-r--r--spec/frontend/blob_edit/blob_bundle_spec.js24
-rw-r--r--spec/frontend/boards/board_list_spec.js11
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js16
-rw-r--r--spec/frontend/boards/components/board_content_spec.js28
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js16
-rw-r--r--spec/frontend/boards/components/issue_board_filtered_search_spec.js8
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js15
-rw-r--r--spec/frontend/boards/mock_data.js40
-rw-r--r--spec/frontend/boards/project_select_spec.js2
-rw-r--r--spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js11
-rw-r--r--spec/frontend/ci/ci_lint/components/ci_lint_spec.js (renamed from spec/frontend/ci_lint/components/ci_lint_spec.js)6
-rw-r--r--spec/frontend/ci/ci_lint/mock_data.js (renamed from spec/frontend/ci_lint/mock_data.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js (renamed from spec/frontend/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js (renamed from spec/frontend/pipeline_editor/components/commit/commit_form_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js (renamed from spec/frontend/pipeline_editor/components/commit/commit_section_spec.js)14
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js (renamed from spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js (renamed from spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js (renamed from spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js (renamed from spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js (renamed from spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js (renamed from spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js (renamed from spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js (renamed from spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js (renamed from spec/frontend/pipeline_editor/components/editor/text_editor_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js (renamed from spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js)12
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js (renamed from spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js)10
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js (renamed from spec/frontend/pipeline_editor/components/file-tree/container_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js (renamed from spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js (renamed from spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js (renamed from spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js (renamed from spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js (renamed from spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js (renamed from spec/frontend/pipeline_editor/components/header/validation_segment_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js (renamed from spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js (renamed from spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js (renamed from spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js)14
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js (renamed from spec/frontend/pipeline_editor/components/popovers/file_tree_popover_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js (renamed from spec/frontend/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js (renamed from spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js (renamed from spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js (renamed from spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js (renamed from spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_messages_spec.js (renamed from spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js)8
-rw-r--r--spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js (renamed from spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js)12
-rw-r--r--spec/frontend/ci/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap (renamed from spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap)2
-rw-r--r--spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js (renamed from spec/frontend/pipeline_editor/graphql/resolvers_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/mock_data.js (renamed from spec/frontend/pipeline_editor/mock_data.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js (renamed from spec/frontend/pipeline_editor/pipeline_editor_app_spec.js)34
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js (renamed from spec/frontend/pipeline_editor/pipeline_editor_home_spec.js)20
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js149
-rw-r--r--spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js (renamed from spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js)4
-rw-r--r--spec/frontend/ci/reports/codequality_report/mock_data.js (renamed from spec/frontend/reports/codequality_report/mock_data.js)0
-rw-r--r--spec/frontend/ci/reports/codequality_report/store/actions_spec.js (renamed from spec/frontend/reports/codequality_report/store/actions_spec.js)8
-rw-r--r--spec/frontend/ci/reports/codequality_report/store/getters_spec.js (renamed from spec/frontend/reports/codequality_report/store/getters_spec.js)6
-rw-r--r--spec/frontend/ci/reports/codequality_report/store/mutations_spec.js (renamed from spec/frontend/reports/codequality_report/store/mutations_spec.js)6
-rw-r--r--spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js (renamed from spec/frontend/reports/codequality_report/store/utils/codequality_parser_spec.js)4
-rw-r--r--spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap (renamed from spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap)0
-rw-r--r--spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap (renamed from spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap)0
-rw-r--r--spec/frontend/ci/reports/components/grouped_issues_list_spec.js (renamed from spec/frontend/reports/components/grouped_issues_list_spec.js)4
-rw-r--r--spec/frontend/ci/reports/components/issue_status_icon_spec.js (renamed from spec/frontend/reports/components/issue_status_icon_spec.js)4
-rw-r--r--spec/frontend/ci/reports/components/report_item_spec.js (renamed from spec/frontend/reports/components/report_item_spec.js)8
-rw-r--r--spec/frontend/ci/reports/components/report_link_spec.js (renamed from spec/frontend/reports/components/report_link_spec.js)4
-rw-r--r--spec/frontend/ci/reports/components/report_section_spec.js (renamed from spec/frontend/reports/components/report_section_spec.js)4
-rw-r--r--spec/frontend/ci/reports/components/summary_row_spec.js (renamed from spec/frontend/reports/components/summary_row_spec.js)2
-rw-r--r--spec/frontend/ci/reports/mock_data/mock_data.js (renamed from spec/frontend/reports/mock_data/mock_data.js)0
-rw-r--r--spec/frontend/ci/reports/mock_data/new_and_fixed_failures_report.json (renamed from spec/frontend/reports/mock_data/new_and_fixed_failures_report.json)23
-rw-r--r--spec/frontend/ci/reports/mock_data/new_errors_report.json (renamed from spec/frontend/reports/mock_data/new_errors_report.json)23
-rw-r--r--spec/frontend/ci/reports/mock_data/new_failures_report.json (renamed from spec/frontend/reports/mock_data/new_failures_report.json)23
-rw-r--r--spec/frontend/ci/reports/mock_data/new_failures_with_null_files_report.json (renamed from spec/frontend/reports/mock_data/new_failures_with_null_files_report.json)23
-rw-r--r--spec/frontend/ci/reports/mock_data/no_failures_report.json (renamed from spec/frontend/reports/mock_data/no_failures_report.json)23
-rw-r--r--spec/frontend/ci/reports/mock_data/recent_failures_report.json (renamed from spec/frontend/reports/mock_data/recent_failures_report.json)26
-rw-r--r--spec/frontend/ci/reports/mock_data/resolved_failures.json (renamed from spec/frontend/reports/mock_data/resolved_failures.json)25
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js51
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js37
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js (renamed from spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js)29
-rw-r--r--spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js3
-rw-r--r--spec/frontend/ci/runner/components/runner_job_status_badge_spec.js51
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js15
-rw-r--r--spec/frontend/ci/runner/components/runner_status_badge_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_stats_spec.js53
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js16
-rw-r--r--spec/frontend/ci/runner/mock_data.js7
-rw-r--r--spec/frontend/ci/runner/runner_search_utils_spec.js16
-rw-r--r--spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js1
-rw-r--r--spec/frontend/ci_variable_list/components/ci_group_variables_spec.js1
-rw-r--r--spec/frontend/ci_variable_list/components/ci_project_variables_spec.js1
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js75
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js27
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js38
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_table_spec.js80
-rw-r--r--spec/frontend/ci_variable_list/mocks.js16
-rw-r--r--spec/frontend/clusters_list/components/agent_token_spec.js40
-rw-r--r--spec/frontend/clusters_list/components/agents_spec.js39
-rw-r--r--spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js95
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js4
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js4
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap8
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js19
-rw-r--r--spec/frontend/content_editor/components/formatting_toolbar_spec.js (renamed from spec/frontend/content_editor/components/top_toolbar_spec.js)4
-rw-r--r--spec/frontend/content_editor/extensions/comment_spec.js30
-rw-r--r--spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js12
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js53
-rw-r--r--spec/frontend/content_editor/test_utils.js4
-rw-r--r--spec/frontend/crm/contact_form_wrapper_spec.js18
-rw-r--r--spec/frontend/crm/crm_form_spec.js (renamed from spec/frontend/crm/form_spec.js)4
-rw-r--r--spec/frontend/crm/organization_form_wrapper_spec.js4
-rw-r--r--spec/frontend/deploy_freeze/store/actions_spec.js88
-rw-r--r--spec/frontend/deploy_tokens/components/new_deploy_token_spec.js72
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js2
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap342
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js19
-rw-r--r--spec/frontend/diffs/components/diff_code_quality_spec.js15
-rw-r--r--spec/frontend/diffs/components/diff_discussion_reply_spec.js36
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js3
-rw-r--r--spec/frontend/diffs/mock_data/diff_code_quality.js28
-rw-r--r--spec/frontend/diffs/store/actions_spec.js44
-rw-r--r--spec/frontend/diffs/utils/merge_request_spec.js40
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_button_spec.js73
-rw-r--r--spec/frontend/editor/schema/ci/ci_schema_spec.js14
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml13
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml31
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/hooks.yml10
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/id_tokens.yml11
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml39
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_options.yml4
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml10
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml17
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/hooks.yml10
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/id_tokens.yml11
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml28
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml9
-rw-r--r--spec/frontend/editor/source_editor_markdown_ext_spec.js14
-rw-r--r--spec/frontend/environment.js11
-rw-r--r--spec/frontend/environments/environment_details_page_spec.js50
-rw-r--r--spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap127
-rw-r--r--spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js96
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_table_spec.js39
-rw-r--r--spec/frontend/feature_flags/components/strategy_label_spec.js61
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_helper_spec.js6
-rw-r--r--spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js3
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js6
-rw-r--r--spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js5
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js4
-rw-r--r--spec/frontend/fixtures/api_merge_requests.rb8
-rw-r--r--spec/frontend/fixtures/api_projects.rb8
-rw-r--r--spec/frontend/fixtures/environments.rb53
-rw-r--r--spec/frontend/fixtures/freeze_period.rb9
-rw-r--r--spec/frontend/fixtures/releases.rb18
-rw-r--r--spec/frontend/fixtures/runner_instructions.rb43
-rw-r--r--spec/frontend/fixtures/tabs.rb8
-rw-r--r--spec/frontend/flash_spec.js24
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js3
-rw-r--r--spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js202
-rw-r--r--spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_spec.js84
-rw-r--r--spec/frontend/gitlab_version_check/index_spec.js138
-rw-r--r--spec/frontend/gitlab_version_check/mock_data.js22
-rw-r--r--spec/frontend/gitlab_version_check/utils_spec.js35
-rw-r--r--spec/frontend/groups/components/app_spec.js66
-rw-r--r--spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js27
-rw-r--r--spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js27
-rw-r--r--spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js (renamed from spec/frontend/groups/components/empty_state_spec.js)18
-rw-r--r--spec/frontend/groups/components/group_name_and_path_spec.js2
-rw-r--r--spec/frontend/groups/components/groups_spec.js10
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js33
-rw-r--r--spec/frontend/header_search/components/app_spec.js1
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js33
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js2
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js94
-rw-r--r--spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js214
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js164
-rw-r--r--spec/frontend/ide/lib/common/model_spec.js9
-rw-r--r--spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js22
-rw-r--r--spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js32
-rw-r--r--spec/frontend/ide/remote/index_spec.js91
-rw-r--r--spec/frontend/ide/services/index_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/terminal/messages_spec.js4
-rw-r--r--spec/frontend/import_entities/components/group_dropdown_spec.js76
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js38
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js44
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js28
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/fixtures.js39
-rw-r--r--spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js66
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js64
-rw-r--r--spec/frontend/import_entities/import_projects/store/actions_spec.js230
-rw-r--r--spec/frontend/import_entities/import_projects/store/getters_spec.js19
-rw-r--r--spec/frontend/import_entities/import_projects/store/mutations_spec.js79
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_service_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js26
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_actions_spec.js227
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js420
-rw-r--r--spec/frontend/invite_members/components/import_project_members_modal_spec.js46
-rw-r--r--spec/frontend/invite_members/components/invite_group_notification_spec.js42
-rw-r--r--spec/frontend/invite_members/components/invite_groups_modal_spec.js63
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js81
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js86
-rw-r--r--spec/frontend/invite_members/mock_data/group_modal.js2
-rw-r--r--spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js54
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js332
-rw-r--r--spec/frontend/issues/dashboard/mock_data.js88
-rw-r--r--spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js68
-rw-r--r--spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js211
-rw-r--r--spec/frontend/issues/list/components/issue_card_statistics_spec.js64
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js192
-rw-r--r--spec/frontend/issues/list/mock_data.js40
-rw-r--r--spec/frontend/issues/list/utils_spec.js28
-rw-r--r--spec/frontend/issues/related_merge_requests/store/actions_spec.js6
-rw-r--r--spec/frontend/issues/show/components/app_spec.js30
-rw-r--r--spec/frontend/issues/show/components/description_spec.js10
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js34
-rw-r--r--spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js8
-rw-r--r--spec/frontend/issues/show/components/incidents/incident_tabs_spec.js83
-rw-r--r--spec/frontend/issues/show/components/incidents/mock_data.js21
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js41
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js27
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js17
-rw-r--r--spec/frontend/issues/show/components/locked_warning_spec.js55
-rw-r--r--spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js82
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js56
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js35
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap20
-rw-r--r--spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js14
-rw-r--r--spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js8
-rw-r--r--spec/frontend/jobs/components/job/empty_state_spec.js19
-rw-r--r--spec/frontend/jobs/components/job/job_app_spec.js175
-rw-r--r--spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js1
-rw-r--r--spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js156
-rw-r--r--spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js109
-rw-r--r--spec/frontend/jobs/components/job/manual_variables_form_spec.js232
-rw-r--r--spec/frontend/jobs/components/job/mock_data.js76
-rw-r--r--spec/frontend/jobs/components/job/sidebar_header_spec.js128
-rw-r--r--spec/frontend/jobs/mock_data.js6
-rw-r--r--spec/frontend/language_switcher/components/app_spec.js62
-rw-r--r--spec/frontend/language_switcher/mock_data.js26
-rw-r--r--spec/frontend/lib/dompurify_spec.js5
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js32
-rw-r--r--spec/frontend/lib/utils/create_and_submit_form_spec.js73
-rw-r--r--spec/frontend/lib/utils/dom_utils_spec.js18
-rw-r--r--spec/frontend/lib/utils/poll_until_complete_spec.js4
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js13
-rw-r--r--spec/frontend/listbox/index_spec.js4
-rw-r--r--spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js13
-rw-r--r--spec/frontend/merge_request_tabs_spec.js1
-rw-r--r--spec/frontend/merge_requests/components/target_project_dropdown_spec.js80
-rw-r--r--spec/frontend/milestones/components/milestone_combobox_spec.js4
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap233
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap (renamed from spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap)73
-rw-r--r--spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js2
-rw-r--r--spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js43
-rw-r--r--spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js (renamed from spec/frontend/ml/experiment_tracking/components/experiment_spec.js)12
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap4
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js14
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js19
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js11
-rw-r--r--spec/frontend/monitoring/utils_spec.js1
-rw-r--r--spec/frontend/nav/components/new_nav_toggle_spec.js98
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js3
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js3
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js110
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js53
-rw-r--r--spec/frontend/notes/stores/actions_spec.js67
-rw-r--r--spec/frontend/observability/observability_app_spec.js186
-rw-r--r--spec/frontend/observability/skeleton_spec.js96
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js21
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js9
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js35
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js3
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js31
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js32
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js3
-rw-r--r--spec/frontend/packages_and_registries/shared/utils_spec.js2
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js117
-rw-r--r--spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js6
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js172
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap88
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js24
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js116
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js211
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_content_spec.js4
-rw-r--r--spec/frontend/performance_bar/components/detailed_metric_spec.js13
-rw-r--r--spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js456
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js4
-rw-r--r--spec/frontend/pipeline_new/mock_data.js4
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js12
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js19
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js15
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js23
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js2
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js7
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_status_token_spec.js8
-rw-r--r--spec/frontend/popovers/components/popovers_spec.js3
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js38
-rw-r--r--spec/frontend/projects/new/components/new_project_url_select_spec.js32
-rw-r--r--spec/frontend/projects/project_new_spec.js55
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js15
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/mock_data.js4
-rw-r--r--spec/frontend/projects/settings/mock_data.js57
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js7
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js20
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/mock_data.js68
-rw-r--r--spec/frontend/projects/settings/utils_spec.js11
-rw-r--r--spec/frontend/releases/__snapshots__/util_spec.js.snap22
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js231
-rw-r--r--spec/frontend/releases/components/release_block_spec.js7
-rw-r--r--spec/frontend/repository/components/table/index_spec.js3
-rw-r--r--spec/frontend/repository/components/table/row_spec.js3
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js1
-rw-r--r--spec/frontend/repository/utils/ref_switcher_utils_spec.js22
-rw-r--r--spec/frontend/search/mock_data.js1
-rw-r--r--spec/frontend/search/sidebar/components/confidentiality_filter_spec.js63
-rw-r--r--spec/frontend/search/sidebar/components/filters_spec.js40
-rw-r--r--spec/frontend/search/sidebar/components/scope_navigation_spec.js41
-rw-r--r--spec/frontend/search/sidebar/components/status_filter_spec.js63
-rw-r--r--spec/frontend/search/topbar/components/app_spec.js76
-rw-r--r--spec/frontend/self_monitor/store/actions_spec.js6
-rw-r--r--spec/frontend/sentry/index_spec.js60
-rw-r--r--spec/frontend/sentry/legacy_index_spec.js64
-rw-r--r--spec/frontend/sentry/legacy_sentry_config_spec.js215
-rw-r--r--spec/frontend/sentry/sentry_browser_wrapper_spec.js59
-rw-r--r--spec/frontend/sentry/sentry_config_spec.js131
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_title_spec.js (renamed from spec/frontend/sidebar/assignee_title_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js (renamed from spec/frontend/sidebar/assignees_realtime_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/assignees/assignees_spec.js (renamed from spec/frontend/sidebar/assignees_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js (renamed from spec/frontend/sidebar/issuable_assignees_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js (renamed from spec/frontend/sidebar/sidebar_assignees_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js4
-rw-r--r--spec/frontend/sidebar/components/copy/copy_email_to_clipboard_spec.js (renamed from spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/copy/copyable_field_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/copyable_field_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js (renamed from spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js (renamed from spec/frontend/sidebar/components/crm_contacts_spec.js)6
-rw-r--r--spec/frontend/sidebar/components/incidents/escalation_status_spec.js6
-rw-r--r--spec/frontend/sidebar/components/incidents/escalation_utils_spec.js4
-rw-r--r--spec/frontend/sidebar/components/incidents/mock_data.js2
-rw-r--r--spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js83
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js)12
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js)6
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js)18
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/mock_data.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js)0
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js)6
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/store/getters_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/store/mutations_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js)8
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js)10
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js77
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js)60
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js)0
-rw-r--r--spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap (renamed from spec/frontend/sidebar/lock/__snapshots__/edit_form_spec.js.snap)0
-rw-r--r--spec/frontend/sidebar/components/lock/constants.js (renamed from spec/frontend/sidebar/lock/constants.js)0
-rw-r--r--spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js (renamed from spec/frontend/sidebar/lock/edit_form_buttons_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/lock/edit_form_spec.js (renamed from spec/frontend/sidebar/lock/edit_form_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js (renamed from spec/frontend/sidebar/lock/issuable_lock_form_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/move/move_issues_button_spec.js (renamed from spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js)26
-rw-r--r--spec/frontend/sidebar/components/participants/participants_spec.js (renamed from spec/frontend/sidebar/participants_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js (renamed from spec/frontend/sidebar/reviewer_title_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/reviewers/reviewers_spec.js (renamed from spec/frontend/sidebar/reviewers_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js77
-rw-r--r--spec/frontend/sidebar/components/severity/severity_spec.js2
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_spec.js4
-rw-r--r--spec/frontend/sidebar/components/status/status_dropdown_spec.js (renamed from spec/frontend/issuable/bulk_update_sidebar/components/status_dropdown_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js (renamed from spec/frontend/issuable/bulk_update_sidebar/components/subscriptions_dropdown_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js (renamed from spec/frontend/sidebar/subscriptions_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js219
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js6
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js63
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap (renamed from spec/frontend/sidebar/__snapshots__/todo_spec.js.snap)0
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js2
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/todo_button_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/todo_spec.js (renamed from spec/frontend/sidebar/todo_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js)2
-rw-r--r--spec/frontend/sidebar/lib/sidebar_move_issue_spec.js (renamed from spec/frontend/sidebar/sidebar_move_issue_spec.js)2
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js58
-rw-r--r--spec/frontend/sidebar/stores/sidebar_store_spec.js (renamed from spec/frontend/sidebar/sidebar_store_spec.js)2
-rw-r--r--spec/frontend/terms/components/app_spec.js7
-rw-r--r--spec/frontend/terraform/components/init_command_modal_spec.js21
-rw-r--r--spec/frontend/token_access/mock_data.js13
-rw-r--r--spec/frontend/token_access/token_access_spec.js109
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js141
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js28
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js47
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js38
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js20
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js55
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js25
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap305
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/actions_button_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js49
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js109
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js (renamed from spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js)104
-rw-r--r--spec/frontend/vue_shared/components/group_select/group_select_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js132
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_view_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/mocks/items_filters.json30
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/registry/registry_search_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/mock_data.js122
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js87
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js53
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/go_sum_linker_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js216
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js2
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js141
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js18
-rw-r--r--spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap4
-rw-r--r--spec/frontend/work_items/components/notes/system_note_spec.js111
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js14
-rw-r--r--spec/frontend/work_items/components/work_item_description_rendered_spec.js8
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js34
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js91
-rw-r--r--spec/frontend/work_items/components/work_item_information_spec.js43
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js14
-rw-r--r--spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js35
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js67
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js161
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js47
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js188
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js147
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_spec.js12
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js107
-rw-r--r--spec/frontend/work_items/mock_data.js588
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js2
-rw-r--r--spec/frontend/work_items/router_spec.js16
-rw-r--r--spec/frontend_integration/content_editor/content_editor_integration_spec.js2
-rw-r--r--spec/graphql/graphql_triggers_spec.rb14
-rw-r--r--spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb9
-rw-r--r--spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb9
-rw-r--r--spec/graphql/mutations/alert_management/create_alert_issue_spec.rb18
-rw-r--r--spec/graphql/mutations/alert_management/update_alert_status_spec.rb9
-rw-r--r--spec/graphql/mutations/ci/runner/bulk_delete_spec.rb2
-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_spec.rb17
-rw-r--r--spec/graphql/mutations/incident_management/timeline_event/update_spec.rb44
-rw-r--r--spec/graphql/mutations/issues/link_alerts_spec.rb84
-rw-r--r--spec/graphql/mutations/issues/unlink_alert_spec.rb83
-rw-r--r--spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb43
-rw-r--r--spec/graphql/resolvers/ci/group_runners_resolver_spec.rb4
-rw-r--r--spec/graphql/resolvers/ci/project_runners_resolver_spec.rb86
-rw-r--r--spec/graphql/resolvers/ci/runner_groups_resolver_spec.rb37
-rw-r--r--spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/runner_status_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/runners_resolver_spec.rb30
-rw-r--r--spec/graphql/resolvers/design_management/design_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/design_management/designs_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/environments/nested_environments_resolver_spec.rb47
-rw-r--r--spec/graphql/resolvers/environments_resolver_spec.rb28
-rw-r--r--spec/graphql/resolvers/namespace_projects_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/paginated_tree_resolver_spec.rb21
-rw-r--r--spec/graphql/resolvers/project_pipeline_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/users/participants_resolver_spec.rb3
-rw-r--r--spec/graphql/types/alert_management/alert_type_spec.rb3
-rw-r--r--spec/graphql/types/ci/detailed_status_type_spec.rb4
-rw-r--r--spec/graphql/types/ci/freeze_period_status_enum_spec.rb9
-rw-r--r--spec/graphql/types/ci/freeze_period_type_spec.rb17
-rw-r--r--spec/graphql/types/ci/pipeline_counts_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/pipeline_schedule_type_spec.rb8
-rw-r--r--spec/graphql/types/ci/pipeline_schedule_variable_type_spec.rb17
-rw-r--r--spec/graphql/types/ci/runner_type_spec.rb2
-rw-r--r--spec/graphql/types/commit_signatures/ssh_signature_type_spec.rb17
-rw-r--r--spec/graphql/types/deployment_details_type_spec.rb17
-rw-r--r--spec/graphql/types/deployment_type_spec.rb7
-rw-r--r--spec/graphql/types/environment_type_spec.rb3
-rw-r--r--spec/graphql/types/issue_type_enum_spec.rb4
-rw-r--r--spec/graphql/types/key_type_spec.rb13
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb4
-rw-r--r--spec/graphql/types/permission_types/base_permission_type_spec.rb21
-rw-r--r--spec/graphql/types/permission_types/deployment_spec.rb11
-rw-r--r--spec/graphql/types/permission_types/environment_spec.rb15
-rw-r--r--spec/graphql/types/permission_types/project_spec.rb2
-rw-r--r--spec/graphql/types/project_type_spec.rb2
-rw-r--r--spec/graphql/types/projects/fork_details_type_spec.rb16
-rw-r--r--spec/graphql/types/projects/service_type_enum_spec.rb1
-rw-r--r--spec/graphql/types/snippets/blob_type_spec.rb19
-rw-r--r--spec/graphql/types/subscription_type_spec.rb1
-rw-r--r--spec/graphql/types/todo_type_spec.rb117
-rw-r--r--spec/graphql/types/work_items/notes_filter_type_enum_spec.rb13
-rw-r--r--spec/graphql/types/work_items/widget_interface_spec.rb1
-rw-r--r--spec/graphql/types/work_items/widgets/hierarchy_type_spec.rb4
-rw-r--r--spec/graphql/types/work_items/widgets/notes_type_spec.rb11
-rw-r--r--spec/haml_lint/linter/documentation_links_spec.rb2
-rw-r--r--spec/helpers/application_helper_spec.rb74
-rw-r--r--spec/helpers/application_settings_helper_spec.rb8
-rw-r--r--spec/helpers/avatars_helper_spec.rb154
-rw-r--r--spec/helpers/blob_helper_spec.rb1
-rw-r--r--spec/helpers/ci/secure_files_helper_spec.rb62
-rw-r--r--spec/helpers/diff_helper_spec.rb31
-rw-r--r--spec/helpers/environment_helper_spec.rb1
-rw-r--r--spec/helpers/git_helper_spec.rb2
-rw-r--r--spec/helpers/groups/observability_helper_spec.rb10
-rw-r--r--spec/helpers/groups/settings_helper_spec.rb5
-rw-r--r--spec/helpers/ide_helper_spec.rb10
-rw-r--r--spec/helpers/invite_members_helper_spec.rb6
-rw-r--r--spec/helpers/issuables_helper_spec.rb60
-rw-r--r--spec/helpers/issues_helper_spec.rb26
-rw-r--r--spec/helpers/labels_helper_spec.rb23
-rw-r--r--spec/helpers/listbox_helper_spec.rb2
-rw-r--r--spec/helpers/markup_helper_spec.rb51
-rw-r--r--spec/helpers/nav/top_nav_helper_spec.rb24
-rw-r--r--spec/helpers/page_layout_helper_spec.rb4
-rw-r--r--spec/helpers/preferred_language_switcher_helper_spec.rb22
-rw-r--r--spec/helpers/programming_languages_helper_spec.rb71
-rw-r--r--spec/helpers/projects/ml/experiments_helper_spec.rb66
-rw-r--r--spec/helpers/projects/pipeline_helper_spec.rb1
-rw-r--r--spec/helpers/projects_helper_spec.rb26
-rw-r--r--spec/helpers/search_helper_spec.rb121
-rw-r--r--spec/helpers/sorting_helper_spec.rb99
-rw-r--r--spec/helpers/tab_helper_spec.rb2
-rw-r--r--spec/helpers/todos_helper_spec.rb118
-rw-r--r--spec/helpers/version_check_helper_spec.rb38
-rw-r--r--spec/helpers/web_hooks/web_hooks_helper_spec.rb39
-rw-r--r--spec/helpers/wiki_helper_spec.rb2
-rw-r--r--spec/helpers/x509_helper_spec.rb18
-rw-r--r--spec/initializers/database_config_spec.rb30
-rw-r--r--spec/initializers/diagnostic_reports_spec.rb32
-rw-r--r--spec/initializers/forbid_sidekiq_in_transactions_spec.rb6
-rw-r--r--spec/initializers/lograge_spec.rb2
-rw-r--r--spec/initializers/rails_yaml_safe_load_spec.rb5
-rw-r--r--spec/lib/api/ci/helpers/runner_helpers_spec.rb2
-rw-r--r--spec/lib/api/entities/package_spec.rb8
-rw-r--r--spec/lib/api/entities/plan_limit_spec.rb3
-rw-r--r--spec/lib/api/entities/ssh_key_spec.rb5
-rw-r--r--spec/lib/api/every_api_endpoint_spec.rb15
-rw-r--r--spec/lib/api/helpers/packages_helpers_spec.rb22
-rw-r--r--spec/lib/api/helpers/rate_limiter_spec.rb3
-rw-r--r--spec/lib/api/support/git_access_actor_spec.rb39
-rw-r--r--spec/lib/atlassian/jira_connect/client_spec.rb93
-rw-r--r--spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb26
-rw-r--r--spec/lib/atlassian/jira_connect/jwt/symmetric_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/author_entity_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/base_entity_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/branch_entity_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/repository_entity_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect_spec.rb10
-rw-r--r--spec/lib/backup/gitaly_backup_spec.rb2
-rw-r--r--spec/lib/backup/manager_spec.rb4
-rw-r--r--spec/lib/banzai/filter/attributes_filter_spec.rb78
-rw-r--r--spec/lib/banzai/filter/commit_trailers_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/inline_observability_filter_spec.rb33
-rw-r--r--spec/lib/banzai/filter/references/reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/user_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/repository_link_filter_spec.rb16
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb15
-rw-r--r--spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/base_parser_spec.rb23
-rw-r--r--spec/lib/bitbucket_server/connection_spec.rb6
-rw-r--r--spec/lib/bulk_imports/clients/http_spec.rb32
-rw-r--r--spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb20
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb1
-rw-r--r--spec/lib/extracts_ref_spec.rb13
-rw-r--r--spec/lib/feature/definition_spec.rb13
-rw-r--r--spec/lib/feature_spec.rb226
-rw-r--r--spec/lib/generators/model/mocks/migration_file.txt2
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb5
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb8
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb4
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events_spec.rb14
-rw-r--r--spec/lib/gitlab/api_authentication/sent_through_builder_spec.rb4
-rw-r--r--spec/lib/gitlab/application_context_spec.rb4
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb16
-rw-r--r--spec/lib/gitlab/audit/auditor_spec.rb28
-rw-r--r--spec/lib/gitlab/audit/type/definition_spec.rb77
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb4
-rw-r--r--spec/lib/gitlab/auth/current_user_mode_spec.rb31
-rw-r--r--spec/lib/gitlab/auth/ldap/config_spec.rb5
-rw-r--r--spec/lib/gitlab/auth/ldap/user_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/saml/user_spec.rb10
-rw-r--r--spec/lib/gitlab/auth/unique_ips_limiter_spec.rb10
-rw-r--r--spec/lib/gitlab/background_migration/backfill_environment_tiers_spec.rb119
-rw-r--r--spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb10
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_namespace_details_spec.rb6
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb20
-rw-r--r--spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/batched_migration_job_spec.rb22
-rw-r--r--spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb18
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb32
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb75
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb71
-rw-r--r--spec/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb61
-rw-r--r--spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb36
-rw-r--r--spec/lib/gitlab/background_migration/populate_container_repository_migration_plan_spec.rb12
-rw-r--r--spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/prune_stale_project_export_jobs_spec.rb63
-rw-r--r--spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb5
-rw-r--r--spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb5
-rw-r--r--spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb6
-rw-r--r--spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb18
-rw-r--r--spec/lib/gitlab/background_migration/reset_status_on_container_repositories_spec.rb261
-rw-r--r--spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb11
-rw-r--r--spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb20
-rw-r--r--spec/lib/gitlab/blob_helper_spec.rb1
-rw-r--r--spec/lib/gitlab/bullet_spec.rb65
-rw-r--r--spec/lib/gitlab/checks/timed_logger_spec.rb44
-rw-r--r--spec/lib/gitlab/ci/build/cache_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/build/context/build_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/build/hook_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/config/entry/artifacts_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/config/entry/bridge_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb72
-rw-r--r--spec/lib/gitlab/ci/config/entry/default_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/config/entry/hooks_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/config/entry/id_token_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb31
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/config/entry/trigger_spec.rb44
-rw-r--r--spec/lib/gitlab/ci/config/entry/variable_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/config/entry/variables_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/config/external/file/remote_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/base_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb72
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb74
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb44
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb137
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb51
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb225
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/environment_matcher_spec.rb51
-rw-r--r--spec/lib/gitlab/ci/lint_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/command_spec.rb57
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/pipeline/logger_spec.rb160
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb33
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb1607
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/sbom/component_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/sbom/report_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/sbom/reports_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/sbom/source_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/security/reports_spec.rb101
-rw-r--r--spec/lib/gitlab/ci/runner_instructions_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/trace/archive_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/variables/builder_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/yaml_processor/result_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb65
-rw-r--r--spec/lib/gitlab/cluster/rack_timeout_observer_spec.rb23
-rw-r--r--spec/lib/gitlab/config/entry/attributable_spec.rb16
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb6
-rw-r--r--spec/lib/gitlab/content_security_policy/config_loader_spec.rb58
-rw-r--r--spec/lib/gitlab/contributions_calendar_spec.rb31
-rw-r--r--spec/lib/gitlab/counters/buffered_counter_spec.rb233
-rw-r--r--spec/lib/gitlab/counters/legacy_counter_spec.rb41
-rw-r--r--spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb22
-rw-r--r--spec/lib/gitlab/data_builder/deployment_spec.rb8
-rw-r--r--spec/lib/gitlab/database/gitlab_schema_spec.rb70
-rw-r--r--spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb45
-rw-r--r--spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb25
-rw-r--r--spec/lib/gitlab/database/lock_writes_manager_spec.rb8
-rw-r--r--spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb334
-rw-r--r--spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb19
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb206
-rw-r--r--spec/lib/gitlab/database/migrations/batched_migration_last_id_spec.rb72
-rw-r--r--spec/lib/gitlab/database/migrations/runner_spec.rb8
-rw-r--r--spec/lib/gitlab/database/migrations/sidekiq_helpers_spec.rb276
-rw-r--r--spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb24
-rw-r--r--spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb2
-rw-r--r--spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb6
-rw-r--r--spec/lib/gitlab/database/postgresql_adapter/dump_schema_versions_mixin_spec.rb3
-rw-r--r--spec/lib/gitlab/database/postgresql_database_tasks/load_schema_versions_mixin_spec.rb3
-rw-r--r--spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb77
-rw-r--r--spec/lib/gitlab/database/reindexing_spec.rb2
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb58
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb22
-rw-r--r--spec/lib/gitlab/database/schema_cleaner_spec.rb9
-rw-r--r--spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb34
-rw-r--r--spec/lib/gitlab/database/tables_truncate_spec.rb94
-rw-r--r--spec/lib/gitlab/database/transaction/context_spec.rb2
-rw-r--r--spec/lib/gitlab/database/type/indifferent_jsonb_spec.rb66
-rw-r--r--spec/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer_spec.rb10
-rw-r--r--spec/lib/gitlab/database_spec.rb52
-rw-r--r--spec/lib/gitlab/diff/file_collection/compare_spec.rb31
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb114
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb6
-rw-r--r--spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb10
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb18
-rw-r--r--spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb8
-rw-r--r--spec/lib/gitlab/email/handler/service_desk_handler_spec.rb9
-rw-r--r--spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb8
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb2
-rw-r--r--spec/lib/gitlab/file_type_detection_spec.rb1
-rw-r--r--spec/lib/gitlab/gfm/uploads_rewriter_spec.rb22
-rw-r--r--spec/lib/gitlab/git/base_error_spec.rb11
-rw-r--r--spec/lib/gitlab/git/cross_repo_comparer_spec.rb117
-rw-r--r--spec/lib/gitlab/git/cross_repo_spec.rb83
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb113
-rw-r--r--spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb5
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb6
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb45
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb32
-rw-r--r--spec/lib/gitlab/gitaly_client/with_feature_flag_actors_spec.rb140
-rw-r--r--spec/lib/gitlab/github_gists_import/importer/gist_importer_spec.rb128
-rw-r--r--spec/lib/gitlab/github_gists_import/importer/gists_importer_spec.rb121
-rw-r--r--spec/lib/gitlab/github_gists_import/representation/gist_spec.rb111
-rw-r--r--spec/lib/gitlab/github_gists_import/status_spec.rb50
-rw-r--r--spec/lib/gitlab/github_import/bulk_importing_spec.rb65
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb117
-rw-r--r--spec/lib/gitlab/github_import/clients/proxy_spec.rb102
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb17
-rw-r--r--spec/lib/gitlab/github_import/importer/issues_importer_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb27
-rw-r--r--spec/lib/gitlab/github_import/importer/labels_importer_spec.rb52
-rw-r--r--spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb8
-rw-r--r--spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb57
-rw-r--r--spec/lib/gitlab/github_import/importer/note_importer_spec.rb13
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb24
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb36
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb17
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/releases_importer_spec.rb41
-rw-r--r--spec/lib/gitlab/github_import/markdown/attachment_spec.rb18
-rw-r--r--spec/lib/gitlab/github_import/page_counter_spec.rb12
-rw-r--r--spec/lib/gitlab/github_import/representation/diff_note_spec.rb115
-rw-r--r--spec/lib/gitlab/github_import/representation/diff_notes/discussion_id_spec.rb84
-rw-r--r--spec/lib/gitlab/gon_helper_spec.rb7
-rw-r--r--spec/lib/gitlab/graphql/limit/field_call_count_spec.rb9
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb383
-rw-r--r--spec/lib/gitlab/http_connection_adapter_spec.rb11
-rw-r--r--spec/lib/gitlab/i18n_spec.rb8
-rw-r--r--spec/lib/gitlab/import/merge_request_helpers_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml17
-rw-r--r--spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/group/tree_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/import_test_coverage_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_writer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/lfs_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/model_configuration_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/project/relation_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb19
-rw-r--r--spec/lib/gitlab/import_export/saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb4
-rw-r--r--spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb21
-rw-r--r--spec/lib/gitlab/instrumentation/redis_base_spec.rb80
-rw-r--r--spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb135
-rw-r--r--spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb77
-rw-r--r--spec/lib/gitlab/instrumentation/redis_spec.rb17
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb39
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb20
-rw-r--r--spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb39
-rw-r--r--spec/lib/gitlab/memory/instrumentation_spec.rb4
-rw-r--r--spec/lib/gitlab/memory/jemalloc_spec.rb49
-rw-r--r--spec/lib/gitlab/memory/reporter_spec.rb206
-rw-r--r--spec/lib/gitlab/memory/reports/heap_dump_spec.rb56
-rw-r--r--spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb94
-rw-r--r--spec/lib/gitlab/memory/reports_daemon_spec.rb85
-rw-r--r--spec/lib/gitlab/memory/watchdog/configuration_spec.rb47
-rw-r--r--spec/lib/gitlab/memory/watchdog/configurator_spec.rb158
-rw-r--r--spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb118
-rw-r--r--spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb29
-rw-r--r--spec/lib/gitlab/memory/watchdog/monitor_state_spec.rb11
-rw-r--r--spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb66
-rw-r--r--spec/lib/gitlab/memory/watchdog_spec.rb233
-rw-r--r--spec/lib/gitlab/merge_requests/message_generator_spec.rb (renamed from spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb)201
-rw-r--r--spec/lib/gitlab/metrics/dashboard/validator_spec.rb52
-rw-r--r--spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/global_search_slis_spec.rb69
-rw-r--r--spec/lib/gitlab/metrics/rails_slis_spec.rb29
-rw-r--r--spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb115
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb15
-rw-r--r--spec/lib/gitlab/metrics/subscribers/ldap_spec.rb124
-rw-r--r--spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb36
-rw-r--r--spec/lib/gitlab/metrics_spec.rb24
-rw-r--r--spec/lib/gitlab/middleware/compressed_json_spec.rb113
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb2
-rw-r--r--spec/lib/gitlab/other_markup_spec.rb29
-rw-r--r--spec/lib/gitlab/pages/cache_control_spec.rb30
-rw-r--r--spec/lib/gitlab/pagination/offset_pagination_spec.rb22
-rw-r--r--spec/lib/gitlab/process_management_spec.rb9
-rw-r--r--spec/lib/gitlab/process_supervisor_spec.rb2
-rw-r--r--spec/lib/gitlab/puma_logging/json_formatter_spec.rb4
-rw-r--r--spec/lib/gitlab/quick_actions/dsl_spec.rb7
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb2
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb22
-rw-r--r--spec/lib/gitlab/repository_archive_rate_limiter_spec.rb3
-rw-r--r--spec/lib/gitlab/repository_cache_adapter_spec.rb3
-rw-r--r--spec/lib/gitlab/search/found_blob_spec.rb5
-rw-r--r--spec/lib/gitlab/shell_spec.rb42
-rw-r--r--spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb92
-rw-r--r--spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb23
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb3
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb3
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb3
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb3
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/query_analyzer_spec.rb3
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb6
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb6
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb3
-rw-r--r--spec/lib/gitlab/slash_commands/application_help_spec.rb4
-rw-r--r--spec/lib/gitlab/slash_commands/deploy_spec.rb2
-rw-r--r--spec/lib/gitlab/sql/pattern_spec.rb10
-rw-r--r--spec/lib/gitlab/ssh/signature_spec.rb129
-rw-r--r--spec/lib/gitlab/ssh_public_key_spec.rb3
-rw-r--r--spec/lib/gitlab/tracking/destinations/snowplow_spec.rb25
-rw-r--r--spec/lib/gitlab/tracking/event_definition_spec.rb10
-rw-r--r--spec/lib/gitlab/tracking/service_ping_context_spec.rb49
-rw-r--r--spec/lib/gitlab/tracking_spec.rb9
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb144
-rw-r--r--spec/lib/gitlab/usage/metric_definition_spec.rb10
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb6
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_merge_request_authors_metric_spec.rb25
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb16
-rw-r--r--spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb6
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb8
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb9
-rw-r--r--spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb137
-rw-r--r--spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb53
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb58
-rw-r--r--spec/lib/gitlab/utils/delegator_override/validator_spec.rb12
-rw-r--r--spec/lib/gitlab/utils/delegator_override_spec.rb12
-rw-r--r--spec/lib/gitlab/utils/override_spec.rb3
-rw-r--r--spec/lib/gitlab/utils/strong_memoize_spec.rb32
-rw-r--r--spec/lib/gitlab/work_items/work_item_hierarchy_spec.rb109
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb162
-rw-r--r--spec/lib/gitlab/x509/signature_spec.rb19
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb4
-rw-r--r--spec/lib/json_web_token/hmac_token_spec.rb20
-rw-r--r--spec/lib/mattermost/session_spec.rb8
-rw-r--r--spec/lib/pager_duty/webhook_payload_parser_spec.rb90
-rw-r--r--spec/lib/peek/views/active_record_spec.rb2
-rw-r--r--spec/lib/sbom/package_url/argument_validator_spec.rb2
-rw-r--r--spec/lib/sbom/package_url/decoder_spec.rb2
-rw-r--r--spec/lib/sbom/package_url/encoder_spec.rb2
-rw-r--r--spec/lib/sbom/package_url/normalizer_spec.rb2
-rw-r--r--spec/lib/sbom/package_url_spec.rb2
-rw-r--r--spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb10
-rw-r--r--spec/lib/security/weak_passwords_spec.rb3
-rw-r--r--spec/lib/serializers/json_spec.rb47
-rw-r--r--spec/lib/sidebars/projects/menus/deployments_menu_spec.rb28
-rw-r--r--spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb31
-rw-r--r--spec/lib/sidebars/projects/menus/monitor_menu_spec.rb32
-rw-r--r--spec/lib/sidebars/projects/menus/repository_menu_spec.rb70
-rw-r--r--spec/lib/system_check/app/gitlab_cable_config_exists_check_spec.rb27
-rw-r--r--spec/lib/system_check/app/gitlab_resque_config_exists_check_spec.rb27
-rw-r--r--spec/lib/system_check/base_check_spec.rb2
-rw-r--r--spec/lib/system_check/sidekiq_check_spec.rb62
-rw-r--r--spec/lib/version_check_spec.rb79
-rw-r--r--spec/mailers/emails/profile_spec.rb42
-rw-r--r--spec/mailers/notify_spec.rb32
-rw-r--r--spec/metrics_server/metrics_server_spec.rb3
-rw-r--r--spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb9
-rw-r--r--spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb32
-rw-r--r--spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb2
-rw-r--r--spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb2
-rw-r--r--spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb3
-rw-r--r--spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb34
-rw-r--r--spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb2
-rw-r--r--spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb2
-rw-r--r--spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb2
-rw-r--r--spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb2
-rw-r--r--spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb2
-rw-r--r--spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb2
-rw-r--r--spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb2
-rw-r--r--spec/migrations/20210713042000_fix_ci_sources_pipelines_index_names_spec.rb2
-rw-r--r--spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb2
-rw-r--r--spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb4
-rw-r--r--spec/migrations/20210804150320_create_base_work_item_types_spec.rb4
-rw-r--r--spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb2
-rw-r--r--spec/migrations/20210811122206_update_external_project_bots_spec.rb2
-rw-r--r--spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb2
-rw-r--r--spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb2
-rw-r--r--spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb4
-rw-r--r--spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb2
-rw-r--r--spec/migrations/20210906100316_drop_temporary_columns_and_triggers_for_ci_build_trace_chunks_spec.rb2
-rw-r--r--spec/migrations/20210906130643_drop_temporary_columns_and_triggers_for_taggings_spec.rb2
-rw-r--r--spec/migrations/20210907013944_cleanup_bigint_conversion_for_ci_builds_metadata_spec.rb2
-rw-r--r--spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb2
-rw-r--r--spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb2
-rw-r--r--spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb2
-rw-r--r--spec/migrations/20210915022415_cleanup_bigint_conversion_for_ci_builds_spec.rb2
-rw-r--r--spec/migrations/20210918201050_remove_old_pending_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb9
-rw-r--r--spec/migrations/20210922021816_drop_int4_columns_for_ci_job_artifacts_spec.rb2
-rw-r--r--spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb2
-rw-r--r--spec/migrations/20210922082019_drop_int4_column_for_events_spec.rb2
-rw-r--r--spec/migrations/20210922091402_drop_int4_column_for_push_event_payloads_spec.rb2
-rw-r--r--spec/migrations/20211006060436_schedule_populate_topics_total_projects_count_cache_spec.rb2
-rw-r--r--spec/migrations/20211012134316_clean_up_migrate_merge_request_diff_commit_users_spec.rb2
-rw-r--r--spec/migrations/20211018152654_schedule_remove_duplicate_vulnerabilities_findings3_spec.rb8
-rw-r--r--spec/migrations/20211028155449_schedule_fix_merge_request_diff_commit_users_migration_spec.rb2
-rw-r--r--spec/migrations/20211101222614_consume_remaining_user_namespace_jobs_spec.rb2
-rw-r--r--spec/migrations/20211110143306_add_not_null_constraint_to_security_findings_uuid_spec.rb6
-rw-r--r--spec/migrations/20211110151350_schedule_drop_invalid_security_findings_spec.rb31
-rw-r--r--spec/migrations/20211116111644_schedule_remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb54
-rw-r--r--spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb2
-rw-r--r--spec/migrations/20211126115449_encrypt_static_objects_external_storage_auth_token_spec.rb2
-rw-r--r--spec/migrations/20211126204445_add_task_to_work_item_types_spec.rb4
-rw-r--r--spec/migrations/20211130165043_backfill_sequence_column_for_sprints_table_spec.rb2
-rw-r--r--spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb2
-rw-r--r--spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb5
-rw-r--r--spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb2
-rw-r--r--spec/migrations/20211210140629_encrypt_static_object_token_spec.rb6
-rw-r--r--spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb2
-rw-r--r--spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb4
-rw-r--r--spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb4
-rw-r--r--spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb4
-rw-r--r--spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb4
-rw-r--r--spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb4
-rw-r--r--spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb25
-rw-r--r--spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb2
-rw-r--r--spec/migrations/20220124130028_dedup_runner_projects_spec.rb3
-rw-r--r--spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb3
-rw-r--r--spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb2
-rw-r--r--spec/migrations/20220202105733_delete_service_template_records_spec.rb2
-rw-r--r--spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb28
-rw-r--r--spec/migrations/20220204194347_encrypt_integration_properties_spec.rb2
-rw-r--r--spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb4
-rw-r--r--spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb2
-rw-r--r--spec/migrations/20220213103859_remove_integrations_type_spec.rb2
-rw-r--r--spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb6
-rw-r--r--spec/migrations/20220222192525_remove_null_releases_spec.rb2
-rw-r--r--spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb2
-rw-r--r--spec/migrations/20220305223212_add_security_training_providers_spec.rb4
-rw-r--r--spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb2
-rw-r--r--spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb2
-rw-r--r--spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb3
-rw-r--r--spec/migrations/20220315171129_cleanup_draft_data_from_faulty_regex_spec.rb2
-rw-r--r--spec/migrations/20220316202640_populate_container_repositories_migration_plan_spec.rb8
-rw-r--r--spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb2
-rw-r--r--spec/migrations/20220322132242_update_pages_onboarding_state_spec.rb2
-rw-r--r--spec/migrations/20220324032250_migrate_shimo_confluence_service_category_spec.rb2
-rw-r--r--spec/migrations/20220324165436_schedule_backfill_project_settings_spec.rb4
-rw-r--r--spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb2
-rw-r--r--spec/migrations/20220331133802_schedule_backfill_topics_title_spec.rb2
-rw-r--r--spec/migrations/20220412143552_consume_remaining_encrypt_integration_property_jobs_spec.rb2
-rw-r--r--spec/migrations/20220416054011_schedule_backfill_project_member_namespace_id_spec.rb4
-rw-r--r--spec/migrations/20220420135946_update_batched_background_migration_arguments_spec.rb2
-rw-r--r--spec/migrations/20220426185933_backfill_deployments_finished_at_spec.rb2
-rw-r--r--spec/migrations/20220502015011_clean_up_fix_merge_request_diff_commit_users_spec.rb2
-rw-r--r--spec/migrations/20220502173045_reset_too_many_tags_skipped_registry_imports_spec.rb2
-rw-r--r--spec/migrations/20220503035221_add_gitlab_schema_to_batched_background_migrations_spec.rb2
-rw-r--r--spec/migrations/20220505044348_fix_automatic_iterations_cadences_start_date_spec.rb2
-rw-r--r--spec/migrations/20220505174658_update_index_on_alerts_to_exclude_null_fingerprints_spec.rb2
-rw-r--r--spec/migrations/20220506154054_create_sync_namespace_details_trigger_spec.rb2
-rw-r--r--spec/migrations/20220512190659_remove_web_hooks_web_hook_logs_web_hook_id_fk_spec.rb2
-rw-r--r--spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb4
-rw-r--r--spec/migrations/20220523171107_drop_deploy_tokens_token_column_spec.rb2
-rw-r--r--spec/migrations/20220524074947_finalize_backfill_null_note_discussion_ids_spec.rb2
-rw-r--r--spec/migrations/20220524184149_create_sync_project_namespace_details_trigger_spec.rb2
-rw-r--r--spec/migrations/20220525221133_schedule_backfill_vulnerability_reads_cluster_agent_spec.rb4
-rw-r--r--spec/migrations/20220601110011_schedule_remove_self_managed_wiki_notes_spec.rb4
-rw-r--r--spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb3
-rw-r--r--spec/migrations/20220606080509_fix_incorrect_job_artifacts_expire_at_spec.rb4
-rw-r--r--spec/migrations/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb3
-rw-r--r--spec/migrations/20220607082910_add_sync_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb3
-rw-r--r--spec/migrations/20220620132300_update_last_run_date_for_iterations_cadences_spec.rb2
-rw-r--r--spec/migrations/20220622080547_backfill_project_statistics_with_container_registry_size_spec.rb4
-rw-r--r--spec/migrations/20220627090231_schedule_disable_legacy_open_source_license_for_inactive_public_projects_spec.rb2
-rw-r--r--spec/migrations/20220627152642_queue_update_delayed_project_removal_to_null_for_user_namespace_spec.rb2
-rw-r--r--spec/migrations/20220628012902_finalise_project_namespace_members_spec.rb4
-rw-r--r--spec/migrations/20220629184402_unset_escalation_policies_for_alert_incidents_spec.rb4
-rw-r--r--spec/migrations/20220715163254_update_notes_in_past_spec.rb2
-rw-r--r--spec/migrations/20220721031446_schedule_disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb2
-rw-r--r--spec/migrations/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb2
-rw-r--r--spec/migrations/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb2
-rw-r--r--spec/migrations/20220725150127_update_jira_tracker_data_deployment_type_based_on_url_spec.rb2
-rw-r--r--spec/migrations/20220801155858_schedule_disable_legacy_open_source_licence_for_recent_public_projects_spec.rb3
-rw-r--r--spec/migrations/20220802114351_reschedule_backfill_container_registry_size_into_project_statistics_spec.rb4
-rw-r--r--spec/migrations/20220802204737_remove_deactivated_user_highest_role_stats_spec.rb2
-rw-r--r--spec/migrations/20220816163444_update_start_date_for_iterations_cadences_spec.rb2
-rw-r--r--spec/migrations/20220819153725_add_vulnerability_advisory_foreign_key_to_sbom_vulnerable_component_versions_spec.rb3
-rw-r--r--spec/migrations/20220819162852_add_sbom_component_version_foreign_key_to_sbom_vulnerable_component_versions_spec.rb3
-rw-r--r--spec/migrations/20220906074449_schedule_disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb6
-rw-r--r--spec/migrations/20220913030624_cleanup_attention_request_related_system_notes_spec.rb2
-rw-r--r--spec/migrations/20220920124709_backfill_internal_on_notes_spec.rb2
-rw-r--r--spec/migrations/20220920180451_schedule_vulnerabilities_feedback_migration_spec.rb31
-rw-r--r--spec/migrations/20220921093355_schedule_backfill_namespace_details_spec.rb2
-rw-r--r--spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb3
-rw-r--r--spec/migrations/20220922143143_schedule_reset_duplicate_ci_runners_token_values_spec.rb2
-rw-r--r--spec/migrations/20220922143634_schedule_reset_duplicate_ci_runners_token_encrypted_values_spec.rb4
-rw-r--r--spec/migrations/20220928225711_schedule_update_ci_pipeline_artifacts_locked_status_spec.rb5
-rw-r--r--spec/migrations/20220929213730_schedule_delete_orphaned_operational_vulnerabilities_spec.rb6
-rw-r--r--spec/migrations/20221002234454_finalize_group_member_namespace_id_migration_spec.rb4
-rw-r--r--spec/migrations/20221004094814_schedule_destroy_invalid_members_spec.rb4
-rw-r--r--spec/migrations/20221008032350_add_password_expiration_migration_spec.rb2
-rw-r--r--spec/migrations/20221012033107_add_password_last_changed_at_to_user_details_spec.rb8
-rw-r--r--spec/migrations/20221013154159_update_invalid_dormant_user_setting_spec.rb2
-rw-r--r--spec/migrations/20221018050323_add_objective_and_keyresult_to_work_item_types_spec.rb4
-rw-r--r--spec/migrations/20221018062308_schedule_backfill_project_namespace_details_spec.rb6
-rw-r--r--spec/migrations/20221018095434_schedule_disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb62
-rw-r--r--spec/migrations/20221018193635_ensure_task_note_renaming_background_migration_finished_spec.rb4
-rw-r--r--spec/migrations/20221021145820_create_routing_table_for_builds_metadata_v2_spec.rb4
-rw-r--r--spec/migrations/20221025043930_change_default_value_on_password_last_changed_at_to_user_details_spec.rb2
-rw-r--r--spec/migrations/20221028022627_add_index_on_password_last_changed_at_to_user_details_spec.rb2
-rw-r--r--spec/migrations/20221101032521_add_default_preferred_language_to_application_settings_spec.rb2
-rw-r--r--spec/migrations/20221101032600_add_text_limit_to_default_preferred_language_on_application_settings_spec.rb2
-rw-r--r--spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb2
-rw-r--r--spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb2
-rw-r--r--spec/migrations/20221104115712_backfill_project_statistics_storage_size_without_uploads_size_spec.rb43
-rw-r--r--spec/migrations/20221110152133_delete_orphans_approval_rules_spec.rb22
-rw-r--r--spec/migrations/20221115173607_ensure_work_item_type_backfill_migration_finished_spec.rb102
-rw-r--r--spec/migrations/20221122132812_schedule_prune_stale_project_export_jobs_spec.rb24
-rw-r--r--spec/migrations/20221123133054_queue_reset_status_on_container_repositories_spec.rb51
-rw-r--r--spec/migrations/20221205151917_schedule_backfill_environment_tier_spec.rb24
-rw-r--r--spec/migrations/20221209110934_update_import_sources_on_application_settings_spec.rb21
-rw-r--r--spec/migrations/20221209110935_fix_update_import_sources_on_application_settings_spec.rb34
-rw-r--r--spec/migrations/20221210154044_update_active_billable_users_index_spec.rb33
-rw-r--r--spec/migrations/active_record/schema_spec.rb2
-rw-r--r--spec/migrations/add_default_project_approval_rules_vuln_allowed_spec.rb2
-rw-r--r--spec/migrations/add_epics_relative_position_spec.rb2
-rw-r--r--spec/migrations/add_new_trail_plans_spec.rb2
-rw-r--r--spec/migrations/add_okr_hierarchy_restrictions_spec.rb35
-rw-r--r--spec/migrations/add_open_source_plan_spec.rb2
-rw-r--r--spec/migrations/add_premium_and_ultimate_plan_limits_spec.rb2
-rw-r--r--spec/migrations/add_triggers_to_integrations_type_new_spec.rb2
-rw-r--r--spec/migrations/add_upvotes_count_index_to_issues_spec.rb2
-rw-r--r--spec/migrations/add_web_hook_calls_to_plan_limits_paid_tiers_spec.rb6
-rw-r--r--spec/migrations/adjust_task_note_rename_background_migration_values_spec.rb2
-rw-r--r--spec/migrations/associate_existing_dast_builds_with_variables_spec.rb2
-rw-r--r--spec/migrations/backfill_all_project_namespaces_spec.rb4
-rw-r--r--spec/migrations/backfill_cadence_id_for_boards_scoped_to_iteration_spec.rb2
-rw-r--r--spec/migrations/backfill_clusters_integration_prometheus_enabled_spec.rb2
-rw-r--r--spec/migrations/backfill_cycle_analytics_aggregations_spec.rb2
-rw-r--r--spec/migrations/backfill_epic_cache_counts_spec.rb2
-rw-r--r--spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb58
-rw-r--r--spec/migrations/backfill_group_features_spec.rb2
-rw-r--r--spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb6
-rw-r--r--spec/migrations/backfill_integrations_type_new_spec.rb6
-rw-r--r--spec/migrations/backfill_issues_upvotes_count_spec.rb2
-rw-r--r--spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb4
-rw-r--r--spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb4
-rw-r--r--spec/migrations/backfill_namespace_id_for_project_routes_spec.rb2
-rw-r--r--spec/migrations/backfill_namespace_id_on_issues_spec.rb2
-rw-r--r--spec/migrations/backfill_nuget_temporary_packages_to_processing_status_spec.rb2
-rw-r--r--spec/migrations/backfill_project_import_level_spec.rb4
-rw-r--r--spec/migrations/backfill_project_namespaces_for_group_spec.rb4
-rw-r--r--spec/migrations/backfill_stage_event_hash_spec.rb2
-rw-r--r--spec/migrations/backfill_user_namespace_spec.rb4
-rw-r--r--spec/migrations/bulk_insert_cluster_enabled_grants_spec.rb2
-rw-r--r--spec/migrations/change_public_projects_cost_factor_spec.rb2
-rw-r--r--spec/migrations/change_task_system_note_wording_to_checklist_item_spec.rb2
-rw-r--r--spec/migrations/change_web_hook_events_default_spec.rb2
-rw-r--r--spec/migrations/clean_up_pending_builds_table_spec.rb3
-rw-r--r--spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb2
-rw-r--r--spec/migrations/cleanup_after_fixing_issue_when_admin_changed_primary_email_spec.rb2
-rw-r--r--spec/migrations/cleanup_after_fixing_regression_with_new_users_emails_spec.rb2
-rw-r--r--spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb3
-rw-r--r--spec/migrations/cleanup_move_container_registry_enabled_to_project_feature_spec.rb2
-rw-r--r--spec/migrations/cleanup_mr_attention_request_todos_spec.rb2
-rw-r--r--spec/migrations/cleanup_orphaned_routes_spec.rb2
-rw-r--r--spec/migrations/cleanup_remaining_orphan_invites_spec.rb2
-rw-r--r--spec/migrations/cleanup_vulnerability_state_transitions_with_same_from_state_to_state_spec.rb13
-rw-r--r--spec/migrations/confirm_security_bot_spec.rb2
-rw-r--r--spec/migrations/confirm_support_bot_user_spec.rb4
-rw-r--r--spec/migrations/delete_migrate_shared_vulnerability_scanners_spec.rb2
-rw-r--r--spec/migrations/delete_security_findings_without_uuid_spec.rb3
-rw-r--r--spec/migrations/disable_expiration_policies_linked_to_no_container_images_spec.rb2
-rw-r--r--spec/migrations/disable_job_token_scope_when_unused_spec.rb2
-rw-r--r--spec/migrations/finalize_invalid_member_cleanup_spec.rb4
-rw-r--r--spec/migrations/finalize_issues_namespace_id_backfilling_spec.rb72
-rw-r--r--spec/migrations/finalize_orphaned_routes_cleanup_spec.rb4
-rw-r--r--spec/migrations/finalize_project_namespaces_backfill_spec.rb4
-rw-r--r--spec/migrations/finalize_routes_backfilling_for_projects_spec.rb4
-rw-r--r--spec/migrations/finalize_traversal_ids_background_migrations_spec.rb2
-rw-r--r--spec/migrations/fix_and_backfill_project_namespaces_for_projects_with_duplicate_name_spec.rb2
-rw-r--r--spec/migrations/fix_batched_migrations_old_format_job_arguments_spec.rb2
-rw-r--r--spec/migrations/generate_customers_dot_jwt_signing_key_spec.rb2
-rw-r--r--spec/migrations/insert_ci_daily_pipeline_schedule_triggers_plan_limits_spec.rb14
-rw-r--r--spec/migrations/migrate_elastic_index_settings_spec.rb2
-rw-r--r--spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb3
-rw-r--r--spec/migrations/move_container_registry_enabled_to_project_features3_spec.rb2
-rw-r--r--spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb2
-rw-r--r--spec/migrations/orphaned_invite_tokens_cleanup_spec.rb2
-rw-r--r--spec/migrations/orphaned_invited_members_cleanup_spec.rb2
-rw-r--r--spec/migrations/populate_audit_event_streaming_verification_token_spec.rb2
-rw-r--r--spec/migrations/populate_dismissal_information_for_vulnerabilities_spec.rb2
-rw-r--r--spec/migrations/populate_operation_visibility_permissions_spec.rb2
-rw-r--r--spec/migrations/populate_releases_access_level_from_repository_spec.rb2
-rw-r--r--spec/migrations/queue_backfill_project_feature_package_registry_access_level_spec.rb4
-rw-r--r--spec/migrations/queue_backfill_user_details_fields_spec.rb4
-rw-r--r--spec/migrations/queue_populate_projects_star_count_spec.rb4
-rw-r--r--spec/migrations/re_schedule_latest_pipeline_id_population_with_all_security_related_artifact_types_spec.rb2
-rw-r--r--spec/migrations/recount_epic_cache_counts_spec.rb2
-rw-r--r--spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_features_spec.rb2
-rw-r--r--spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb2
-rw-r--r--spec/migrations/remove_duplicate_dast_site_tokens_spec.rb4
-rw-r--r--spec/migrations/remove_duplicate_dast_site_tokens_with_same_token_spec.rb6
-rw-r--r--spec/migrations/remove_flowdock_integration_records_spec.rb23
-rw-r--r--spec/migrations/remove_hipchat_service_records_spec.rb2
-rw-r--r--spec/migrations/remove_invalid_integrations_spec.rb2
-rw-r--r--spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb2
-rw-r--r--spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb2
-rw-r--r--spec/migrations/remove_schedule_and_status_from_pending_alert_escalations_spec.rb2
-rw-r--r--spec/migrations/remove_wiki_notes_spec.rb2
-rw-r--r--spec/migrations/rename_services_to_integrations_spec.rb2
-rw-r--r--spec/migrations/replace_external_wiki_triggers_spec.rb2
-rw-r--r--spec/migrations/reschedule_backfill_imported_issue_search_data_spec.rb23
-rw-r--r--spec/migrations/reschedule_delete_orphaned_deployments_spec.rb3
-rw-r--r--spec/migrations/reschedule_issue_work_item_type_id_backfill_spec.rb10
-rw-r--r--spec/migrations/reschedule_migrate_shared_vulnerability_scanners_spec.rb2
-rw-r--r--spec/migrations/reset_job_token_scope_enabled_again_spec.rb2
-rw-r--r--spec/migrations/reset_job_token_scope_enabled_spec.rb2
-rw-r--r--spec/migrations/reset_severity_levels_to_new_default_spec.rb2
-rw-r--r--spec/migrations/retry_backfill_traversal_ids_spec.rb4
-rw-r--r--spec/migrations/sanitize_confidential_note_todos_spec.rb2
-rw-r--r--spec/migrations/schedule_add_primary_email_to_emails_if_user_confirmed_spec.rb2
-rw-r--r--spec/migrations/schedule_backfill_cluster_agents_has_vulnerabilities_spec.rb4
-rw-r--r--spec/migrations/schedule_backfill_draft_status_on_merge_requests_corrected_regex_spec.rb2
-rw-r--r--spec/migrations/schedule_backfilling_the_namespace_id_for_vulnerability_reads_spec.rb4
-rw-r--r--spec/migrations/schedule_copy_ci_builds_columns_to_security_scans2_spec.rb2
-rw-r--r--spec/migrations/schedule_disable_expiration_policies_linked_to_no_container_images_spec.rb32
-rw-r--r--spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb2
-rw-r--r--spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb2
-rw-r--r--spec/migrations/schedule_fixing_security_scan_statuses_spec.rb77
-rw-r--r--spec/migrations/schedule_populate_requirements_issue_id_spec.rb4
-rw-r--r--spec/migrations/schedule_purging_stale_security_scans_spec.rb23
-rw-r--r--spec/migrations/schedule_recalculate_vulnerability_finding_signatures_for_findings_spec.rb35
-rw-r--r--spec/migrations/schedule_security_setting_creation_spec.rb2
-rw-r--r--spec/migrations/schedule_set_correct_vulnerability_state_spec.rb4
-rw-r--r--spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb24
-rw-r--r--spec/migrations/schedule_update_timelogs_project_id_spec.rb2
-rw-r--r--spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb2
-rw-r--r--spec/migrations/set_default_job_token_scope_true_spec.rb2
-rw-r--r--spec/migrations/set_email_confirmation_setting_before_removing_send_user_confirmation_email_column_spec.rb41
-rw-r--r--spec/migrations/set_email_confirmation_setting_from_send_user_confirmation_email_setting_spec.rb2
-rw-r--r--spec/migrations/slice_merge_request_diff_commit_migrations_spec.rb2
-rw-r--r--spec/migrations/start_backfill_ci_queuing_tables_spec.rb3
-rw-r--r--spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb2
-rw-r--r--spec/migrations/sync_new_amount_used_for_ci_namespace_monthly_usages_spec.rb3
-rw-r--r--spec/migrations/sync_new_amount_used_for_ci_project_monthly_usages_spec.rb3
-rw-r--r--spec/migrations/toggle_vsa_aggregations_enable_spec.rb2
-rw-r--r--spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb3
-rw-r--r--spec/migrations/update_application_settings_protected_paths_spec.rb9
-rw-r--r--spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb2
-rw-r--r--spec/migrations/update_integrations_trigger_type_new_on_insert_spec.rb2
-rw-r--r--spec/migrations/update_invalid_member_states_spec.rb2
-rw-r--r--spec/migrations/update_invalid_web_hooks_spec.rb2
-rw-r--r--spec/models/achievements/achievement_spec.rb33
-rw-r--r--spec/models/appearance_spec.rb1
-rw-r--r--spec/models/application_setting_spec.rb9
-rw-r--r--spec/models/badge_spec.rb4
-rw-r--r--spec/models/blob_viewer/metrics_dashboard_yml_spec.rb245
-rw-r--r--spec/models/bulk_imports/tracker_spec.rb5
-rw-r--r--spec/models/ci/bridge_spec.rb20
-rw-r--r--spec/models/ci/build_metadata_spec.rb161
-rw-r--r--spec/models/ci/build_need_spec.rb60
-rw-r--r--spec/models/ci/build_pending_state_spec.rb29
-rw-r--r--spec/models/ci/build_report_result_spec.rb32
-rw-r--r--spec/models/ci/build_runner_session_spec.rb18
-rw-r--r--spec/models/ci/build_spec.rb132
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb33
-rw-r--r--spec/models/ci/build_trace_metadata_spec.rb22
-rw-r--r--spec/models/ci/freeze_period_spec.rb129
-rw-r--r--spec/models/ci/freeze_period_status_spec.rb71
-rw-r--r--spec/models/ci/job_artifact_spec.rb44
-rw-r--r--spec/models/ci/job_token/allowlist_spec.rb81
-rw-r--r--spec/models/ci/job_token/project_scope_link_spec.rb16
-rw-r--r--spec/models/ci/job_token/scope_spec.rb66
-rw-r--r--spec/models/ci/job_variable_spec.rb62
-rw-r--r--spec/models/ci/pending_build_spec.rb22
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb65
-rw-r--r--spec/models/ci/pipeline_spec.rb46
-rw-r--r--spec/models/ci/processable_spec.rb5
-rw-r--r--spec/models/ci/resource_group_spec.rb20
-rw-r--r--spec/models/ci/runner_namespace_spec.rb23
-rw-r--r--spec/models/ci/runner_spec.rb26
-rw-r--r--spec/models/ci/runner_version_spec.rb18
-rw-r--r--spec/models/ci/running_build_spec.rb24
-rw-r--r--spec/models/ci/secure_file_spec.rb33
-rw-r--r--spec/models/ci/sources/pipeline_spec.rb18
-rw-r--r--spec/models/ci/unit_test_failure_spec.rb42
-rw-r--r--spec/models/clusters/agent_token_spec.rb8
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb10
-rw-r--r--spec/models/clusters/applications/knative_spec.rb12
-rw-r--r--spec/models/commit_signatures/gpg_signature_spec.rb5
-rw-r--r--spec/models/commit_signatures/ssh_signature_spec.rb7
-rw-r--r--spec/models/commit_signatures/x509_commit_signature_spec.rb5
-rw-r--r--spec/models/commit_spec.rb17
-rw-r--r--spec/models/concerns/batch_destroy_dependent_associations_spec.rb6
-rw-r--r--spec/models/concerns/bulk_insertable_associations_spec.rb47
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb18
-rw-r--r--spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb80
-rw-r--r--spec/models/concerns/ci/partitionable_spec.rb24
-rw-r--r--spec/models/concerns/commit_signature_spec.rb21
-rw-r--r--spec/models/concerns/counter_attribute_spec.rb97
-rw-r--r--spec/models/concerns/has_user_type_spec.rb8
-rw-r--r--spec/models/concerns/pg_full_text_searchable_spec.rb28
-rw-r--r--spec/models/concerns/schedulable_spec.rb2
-rw-r--r--spec/models/concerns/sensitive_serializable_hash_spec.rb33
-rw-r--r--spec/models/concerns/signature_type_spec.rb15
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb1
-rw-r--r--spec/models/concerns/triggerable_hooks_spec.rb6
-rw-r--r--spec/models/container_repository_spec.rb26
-rw-r--r--spec/models/deploy_token_spec.rb6
-rw-r--r--spec/models/deployment_spec.rb15
-rw-r--r--spec/models/design_management/version_spec.rb8
-rw-r--r--spec/models/environment_spec.rb115
-rw-r--r--spec/models/event_spec.rb53
-rw-r--r--spec/models/factories_spec.rb2
-rw-r--r--spec/models/generic_commit_status_spec.rb30
-rw-r--r--spec/models/group_deploy_key_spec.rb12
-rw-r--r--spec/models/group_spec.rb92
-rw-r--r--spec/models/hooks/active_hook_filter_spec.rb18
-rw-r--r--spec/models/hooks/service_hook_spec.rb26
-rw-r--r--spec/models/hooks/web_hook_spec.rb64
-rw-r--r--spec/models/integration_spec.rb4
-rw-r--r--spec/models/integrations/base_chat_notification_spec.rb29
-rw-r--r--spec/models/integrations/base_slack_notification_spec.rb16
-rw-r--r--spec/models/integrations/chat_message/pipeline_message_spec.rb4
-rw-r--r--spec/models/integrations/drone_ci_spec.rb12
-rw-r--r--spec/models/integrations/flowdock_spec.rb54
-rw-r--r--spec/models/integrations/jira_spec.rb33
-rw-r--r--spec/models/integrations/slack_spec.rb6
-rw-r--r--spec/models/issue_collection_spec.rb71
-rw-r--r--spec/models/issue_spec.rb42
-rw-r--r--spec/models/jira_connect_installation_spec.rb16
-rw-r--r--spec/models/jira_import_state_spec.rb2
-rw-r--r--spec/models/key_spec.rb16
-rw-r--r--spec/models/lfs_object_spec.rb54
-rw-r--r--spec/models/member_spec.rb113
-rw-r--r--spec/models/members/group_member_spec.rb34
-rw-r--r--spec/models/members/member_role_spec.rb33
-rw-r--r--spec/models/members/project_member_spec.rb21
-rw-r--r--spec/models/merge_request_diff_spec.rb44
-rw-r--r--spec/models/merge_request_spec.rb27
-rw-r--r--spec/models/milestone_note_spec.rb2
-rw-r--r--spec/models/ml/candidate_metadata_spec.rb20
-rw-r--r--spec/models/ml/candidate_spec.rb41
-rw-r--r--spec/models/ml/experiment_metadata_spec.rb20
-rw-r--r--spec/models/ml/experiment_spec.rb1
-rw-r--r--spec/models/namespace_setting_spec.rb57
-rw-r--r--spec/models/namespace_spec.rb25
-rw-r--r--spec/models/note_spec.rb1
-rw-r--r--spec/models/notification_recipient_spec.rb2
-rw-r--r--spec/models/packages/package_file_spec.rb10
-rw-r--r--spec/models/packages/package_spec.rb12
-rw-r--r--spec/models/packages/rpm/repository_file_spec.rb28
-rw-r--r--spec/models/pages/lookup_path_spec.rb6
-rw-r--r--spec/models/pages_deployment_spec.rb2
-rw-r--r--spec/models/performance_monitoring/prometheus_dashboard_spec.rb84
-rw-r--r--spec/models/plan_limits_spec.rb1
-rw-r--r--spec/models/programming_language_spec.rb21
-rw-r--r--spec/models/project_export_job_spec.rb52
-rw-r--r--spec/models/project_feature_spec.rb1
-rw-r--r--spec/models/project_spec.rb178
-rw-r--r--spec/models/project_statistics_spec.rb18
-rw-r--r--spec/models/projects/build_artifacts_size_refresh_spec.rb6
-rw-r--r--spec/models/projects/forks/divergence_counts_spec.rb98
-rw-r--r--spec/models/release_highlight_spec.rb4
-rw-r--r--spec/models/repository_spec.rb62
-rw-r--r--spec/models/service_desk_setting_spec.rb9
-rw-r--r--spec/models/snippet_repository_spec.rb6
-rw-r--r--spec/models/state_note_spec.rb7
-rw-r--r--spec/models/todo_spec.rb20
-rw-r--r--spec/models/user_detail_spec.rb17
-rw-r--r--spec/models/user_preference_spec.rb157
-rw-r--r--spec/models/user_spec.rb254
-rw-r--r--spec/models/users/phone_number_validation_spec.rb38
-rw-r--r--spec/models/work_item_spec.rb57
-rw-r--r--spec/models/work_items/hierarchy_restriction_spec.rb18
-rw-r--r--spec/models/work_items/parent_link_spec.rb105
-rw-r--r--spec/models/work_items/type_spec.rb31
-rw-r--r--spec/models/work_items/widgets/notes_spec.rb20
-rw-r--r--spec/models/zoom_meeting_spec.rb1
-rw-r--r--spec/policies/ci/runner_policy_spec.rb2
-rw-r--r--spec/policies/concerns/archived_abilities_spec.rb27
-rw-r--r--spec/policies/concerns/readonly_abilities_spec.rb27
-rw-r--r--spec/policies/group_policy_spec.rb53
-rw-r--r--spec/policies/issue_policy_spec.rb44
-rw-r--r--spec/policies/merge_request_policy_spec.rb102
-rw-r--r--spec/policies/namespaces/user_namespace_policy_spec.rb7
-rw-r--r--spec/policies/note_policy_spec.rb29
-rw-r--r--spec/policies/project_policy_spec.rb81
-rw-r--r--spec/presenters/blob_presenter_spec.rb2
-rw-r--r--spec/presenters/ci/freeze_period_presenter_spec.rb37
-rw-r--r--spec/presenters/group_member_presenter_spec.rb18
-rw-r--r--spec/presenters/member_presenter_spec.rb14
-rw-r--r--spec/presenters/packages/pypi/simple_package_versions_presenter_spec.rb8
-rw-r--r--spec/presenters/project_member_presenter_spec.rb18
-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/search_service_presenter_spec.rb8
-rw-r--r--spec/requests/abuse_reports_controller_spec.rb2
-rw-r--r--spec/requests/admin/applications_controller_spec.rb3
-rw-r--r--spec/requests/admin/background_migrations_controller_spec.rb2
-rw-r--r--spec/requests/admin/batched_jobs_controller_spec.rb2
-rw-r--r--spec/requests/admin/broadcast_messages_controller_spec.rb2
-rw-r--r--spec/requests/admin/clusters/integrations_controller_spec.rb2
-rw-r--r--spec/requests/admin/hook_logs_controller_spec.rb2
-rw-r--r--spec/requests/admin/impersonation_tokens_controller_spec.rb3
-rw-r--r--spec/requests/admin/integrations_controller_spec.rb2
-rw-r--r--spec/requests/admin/version_check_controller_spec.rb2
-rw-r--r--spec/requests/api/access_requests_spec.rb2
-rw-r--r--spec/requests/api/admin/batched_background_migrations_spec.rb2
-rw-r--r--spec/requests/api/admin/instance_clusters_spec.rb2
-rw-r--r--spec/requests/api/admin/plan_limits_spec.rb14
-rw-r--r--spec/requests/api/admin/sidekiq_spec.rb2
-rw-r--r--spec/requests/api/alert_management_alerts_spec.rb2
-rw-r--r--spec/requests/api/api_guard/admin_mode_middleware_spec.rb2
-rw-r--r--spec/requests/api/api_guard/response_coercer_middleware_spec.rb2
-rw-r--r--spec/requests/api/api_spec.rb2
-rw-r--r--spec/requests/api/appearance_spec.rb5
-rw-r--r--spec/requests/api/applications_spec.rb2
-rw-r--r--spec/requests/api/avatar_spec.rb2
-rw-r--r--spec/requests/api/award_emoji_spec.rb2
-rw-r--r--spec/requests/api/badges_spec.rb2
-rw-r--r--spec/requests/api/boards_spec.rb2
-rw-r--r--spec/requests/api/branches_spec.rb14
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb2
-rw-r--r--spec/requests/api/bulk_imports_spec.rb5
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb2
-rw-r--r--spec/requests/api/ci/jobs_spec.rb153
-rw-r--r--spec/requests/api/ci/pipeline_schedules_spec.rb2
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb2
-rw-r--r--spec/requests/api/ci/resource_groups_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/jobs_artifacts_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/jobs_put_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb20
-rw-r--r--spec/requests/api/ci/runner/jobs_trace_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/runners_delete_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/runners_post_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/runners_reset_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/runners_verify_post_spec.rb2
-rw-r--r--spec/requests/api/ci/runners_reset_registration_token_spec.rb2
-rw-r--r--spec/requests/api/ci/runners_spec.rb2
-rw-r--r--spec/requests/api/ci/secure_files_spec.rb19
-rw-r--r--spec/requests/api/ci/triggers_spec.rb2
-rw-r--r--spec/requests/api/ci/variables_spec.rb2
-rw-r--r--spec/requests/api/clusters/agent_tokens_spec.rb38
-rw-r--r--spec/requests/api/clusters/agents_spec.rb2
-rw-r--r--spec/requests/api/commit_statuses_spec.rb62
-rw-r--r--spec/requests/api/commits_spec.rb92
-rw-r--r--spec/requests/api/composer_packages_spec.rb23
-rw-r--r--spec/requests/api/conan_instance_packages_spec.rb4
-rw-r--r--spec/requests/api/conan_project_packages_spec.rb47
-rw-r--r--spec/requests/api/container_registry_event_spec.rb2
-rw-r--r--spec/requests/api/container_repositories_spec.rb9
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb2
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb2
-rw-r--r--spec/requests/api/dependency_proxy_spec.rb2
-rw-r--r--spec/requests/api/deploy_keys_spec.rb2
-rw-r--r--spec/requests/api/deploy_tokens_spec.rb12
-rw-r--r--spec/requests/api/deployments_spec.rb46
-rw-r--r--spec/requests/api/discussions_spec.rb69
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb2
-rw-r--r--spec/requests/api/environments_spec.rb6
-rw-r--r--spec/requests/api/error_tracking/client_keys_spec.rb2
-rw-r--r--spec/requests/api/error_tracking/collector_spec.rb2
-rw-r--r--spec/requests/api/error_tracking/project_settings_spec.rb2
-rw-r--r--spec/requests/api/events_spec.rb2
-rw-r--r--spec/requests/api/feature_flags_spec.rb2
-rw-r--r--spec/requests/api/feature_flags_user_lists_spec.rb2
-rw-r--r--spec/requests/api/features_spec.rb52
-rw-r--r--spec/requests/api/files_spec.rb171
-rw-r--r--spec/requests/api/freeze_periods_spec.rb2
-rw-r--r--spec/requests/api/generic_packages_spec.rb8
-rw-r--r--spec/requests/api/geo_spec.rb2
-rw-r--r--spec/requests/api/go_proxy_spec.rb2
-rw-r--r--spec/requests/api/graphql/boards/board_list_issues_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/boards/board_list_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/boards/board_lists_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/boards/boards_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/application_setting_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/ci_cd_setting_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/config_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/config_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/group_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/groups_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/instance_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/job_artifacts_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/job_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb63
-rw-r--r--spec/requests/api/graphql/ci/manual_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/pipeline_schedules_spec.rb58
-rw-r--r--spec/requests/api/graphql/ci/pipelines_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/project_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb264
-rw-r--r--spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb4
-rw-r--r--spec/requests/api/graphql/ci/stages_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/template_spec.rb2
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb2
-rw-r--r--spec/requests/api/graphql/crm/contacts_spec.rb2
-rw-r--r--spec/requests/api/graphql/current_user/groups_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/current_user/todos_query_spec.rb4
-rw-r--r--spec/requests/api/graphql/current_user_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/current_user_todos_spec.rb3
-rw-r--r--spec/requests/api/graphql/custom_emoji_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/environments/deployments_spec.rb (renamed from spec/requests/api/graphql/environments/deployments_query_spec.rb)39
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/container_repositories_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/group_members_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/issues_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/labels_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/merge_requests_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/milestones_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/packages_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/recent_issue_boards_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/timelogs_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/work_item_types_spec.rb2
-rw-r--r--spec/requests/api/graphql/group_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/issue/issue_spec.rb2
-rw-r--r--spec/requests/api/graphql/issue_status_counts_spec.rb2
-rw-r--r--spec/requests/api/graphql/issues_spec.rb128
-rw-r--r--spec/requests/api/graphql/jobs_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/merge_request/merge_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/metadata_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb2
-rw-r--r--spec/requests/api/graphql/metrics/dashboard_query_spec.rb155
-rw-r--r--spec/requests/api/graphql/milestone_spec.rb2
-rw-r--r--spec/requests/api/graphql/multiplexed_queries_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/add_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/boards/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/boards/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/boards/lists/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/boards/lists/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/branches/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/artifacts_destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_play_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_retry_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb151
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb80
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/commits/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb21
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/design_management/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/design_management/move_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/design_management/upload_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/groups/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb43
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb65
-rw-r--r--spec/requests/api/graphql/mutations/issues/move_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_locked_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_severity_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_subscription_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb89
-rw-r--r--spec/requests/api/graphql/mutations/issues/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/jira_import/start_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/labels/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/note_spec.rb31
-rw-r--r--spec/requests/api/graphql/mutations/notes/destroy_spec.rb41
-rw-r--r--spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/update/note_spec.rb59
-rw-r--r--spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/packages/destroy_file_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/packages/destroy_files_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/packages/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/releases/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/releases/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/releases/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/snippets/destroy_spec.rb2
-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.rb15
-rw-r--r--spec/requests/api/graphql/mutations/timelogs/create_spec.rb10
-rw-r--r--spec/requests/api/graphql/mutations/timelogs/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/todos/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/todos/mark_done_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/todos/restore_many_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/todos/restore_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/uploads/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/user_callouts/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/user_preferences/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/work_items/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb10
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_task_spec.rb2
-rw-r--r--spec/requests/api/graphql/namespace/package_settings_spec.rb2
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb2
-rw-r--r--spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb2
-rw-r--r--spec/requests/api/graphql/namespace_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/composer_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/conan_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/helm_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/maven_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/nuget_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb13
-rw-r--r--spec/requests/api/graphql/packages/pypi_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/alert_management/alerts_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/alert_management/integrations_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/base_service_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/branch_rules_spec.rb81
-rw-r--r--spec/requests/api/graphql/project/cluster_agents_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/container_expiration_policy_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/deployment_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/environments_spec.rb133
-rw-r--r--spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_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/fork_details_spec.rb60
-rw-r--r--spec/requests/api/graphql/project/fork_targets_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/grafana_integration_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/version_spec.rb11
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue/designs/designs_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue/designs/notes_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue/notes_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issue_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb698
-rw-r--r--spec/requests/api/graphql/project/jira_import_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/jira_projects_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/jira_service_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/job_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/jobs_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/labels_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/languages_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/merge_request/pipelines_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/milestones_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/packages_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/pipeline_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/project_members_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/project_statistics_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/recent_issue_boards_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/releases_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/repository/blobs_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/repository_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/runners_spec.rb68
-rw-r--r--spec/requests/api/graphql/project/terraform/state_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/terraform/states_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/tree/tree_spec.rb50
-rw-r--r--spec/requests/api/graphql/project/work_item_types_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb134
-rw-r--r--spec/requests/api/graphql/project_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/query_spec.rb2
-rw-r--r--spec/requests/api/graphql/read_only_spec.rb2
-rw-r--r--spec/requests/api/graphql/snippets_spec.rb2
-rw-r--r--spec/requests/api/graphql/tasks/task_completion_status_spec.rb2
-rw-r--r--spec/requests/api/graphql/terraform/state/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/terraform/state/lock_spec.rb2
-rw-r--r--spec/requests/api/graphql/terraform/state/unlock_spec.rb2
-rw-r--r--spec/requests/api/graphql/todo_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/usage_trends_measurements_spec.rb2
-rw-r--r--spec/requests/api/graphql/user/group_member_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/user/project_member_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/user/starred_projects_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/user_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/user_spec.rb2
-rw-r--r--spec/requests/api/graphql/users_spec.rb2
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb12
-rw-r--r--spec/requests/api/graphql_spec.rb6
-rw-r--r--spec/requests/api/group_avatar_spec.rb2
-rw-r--r--spec/requests/api/group_boards_spec.rb2
-rw-r--r--spec/requests/api/group_clusters_spec.rb2
-rw-r--r--spec/requests/api/group_container_repositories_spec.rb6
-rw-r--r--spec/requests/api/group_debian_distributions_spec.rb2
-rw-r--r--spec/requests/api/group_export_spec.rb2
-rw-r--r--spec/requests/api/group_import_spec.rb8
-rw-r--r--spec/requests/api/group_labels_spec.rb2
-rw-r--r--spec/requests/api/group_milestones_spec.rb2
-rw-r--r--spec/requests/api/group_packages_spec.rb2
-rw-r--r--spec/requests/api/group_variables_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb2
-rw-r--r--spec/requests/api/helm_packages_spec.rb8
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/import_bitbucket_server_spec.rb2
-rw-r--r--spec/requests/api/import_github_spec.rb2
-rw-r--r--spec/requests/api/integrations/jira_connect/subscriptions_spec.rb16
-rw-r--r--spec/requests/api/integrations_spec.rb2
-rw-r--r--spec/requests/api/internal/base_spec.rb48
-rw-r--r--spec/requests/api/internal/container_registry/migration_spec.rb2
-rw-r--r--spec/requests/api/internal/error_tracking_spec.rb2
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb18
-rw-r--r--spec/requests/api/internal/lfs_spec.rb2
-rw-r--r--spec/requests/api/internal/mail_room_spec.rb2
-rw-r--r--spec/requests/api/internal/pages_spec.rb2
-rw-r--r--spec/requests/api/internal/workhorse_spec.rb2
-rw-r--r--spec/requests/api/invitations_spec.rb2
-rw-r--r--spec/requests/api/issue_links_spec.rb6
-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.rb2
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb2
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb2
-rw-r--r--spec/requests/api/keys_spec.rb2
-rw-r--r--spec/requests/api/labels_spec.rb2
-rw-r--r--spec/requests/api/lint_spec.rb2
-rw-r--r--spec/requests/api/markdown_golden_master_spec.rb2
-rw-r--r--spec/requests/api/markdown_snapshot_spec.rb2
-rw-r--r--spec/requests/api/markdown_spec.rb2
-rw-r--r--spec/requests/api/maven_packages_spec.rb9
-rw-r--r--spec/requests/api/members_spec.rb2
-rw-r--r--spec/requests/api/merge_request_approvals_spec.rb83
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb120
-rw-r--r--spec/requests/api/metadata_spec.rb2
-rw-r--r--spec/requests/api/metrics/dashboard/annotations_spec.rb2
-rw-r--r--spec/requests/api/metrics/user_starred_dashboards_spec.rb2
-rw-r--r--spec/requests/api/ml/mlflow_spec.rb135
-rw-r--r--spec/requests/api/namespaces_spec.rb2
-rw-r--r--spec/requests/api/notes_spec.rb47
-rw-r--r--spec/requests/api/notification_settings_spec.rb2
-rw-r--r--spec/requests/api/npm_instance_packages_spec.rb14
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb17
-rw-r--r--spec/requests/api/nuget_group_packages_spec.rb6
-rw-r--r--spec/requests/api/nuget_project_packages_spec.rb56
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb4
-rw-r--r--spec/requests/api/package_files_spec.rb2
-rw-r--r--spec/requests/api/pages/internal_access_spec.rb2
-rw-r--r--spec/requests/api/pages/pages_spec.rb2
-rw-r--r--spec/requests/api/pages/private_access_spec.rb2
-rw-r--r--spec/requests/api/pages/public_access_spec.rb2
-rw-r--r--spec/requests/api/pages_domains_spec.rb2
-rw-r--r--spec/requests/api/performance_bar_spec.rb3
-rw-r--r--spec/requests/api/personal_access_tokens/self_information_spec.rb2
-rw-r--r--spec/requests/api/personal_access_tokens_spec.rb2
-rw-r--r--spec/requests/api/project_attributes.yml6
-rw-r--r--spec/requests/api/project_clusters_spec.rb2
-rw-r--r--spec/requests/api/project_container_repositories_spec.rb34
-rw-r--r--spec/requests/api/project_debian_distributions_spec.rb8
-rw-r--r--spec/requests/api/project_events_spec.rb2
-rw-r--r--spec/requests/api/project_export_spec.rb2
-rw-r--r--spec/requests/api/project_hooks_spec.rb2
-rw-r--r--spec/requests/api/project_import_spec.rb46
-rw-r--r--spec/requests/api/project_milestones_spec.rb2
-rw-r--r--spec/requests/api/project_packages_spec.rb12
-rw-r--r--spec/requests/api/project_repository_storage_moves_spec.rb2
-rw-r--r--spec/requests/api/project_snapshots_spec.rb4
-rw-r--r--spec/requests/api/project_snippets_spec.rb2
-rw-r--r--spec/requests/api/project_statistics_spec.rb2
-rw-r--r--spec/requests/api/project_templates_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb44
-rw-r--r--spec/requests/api/protected_branches_spec.rb2
-rw-r--r--spec/requests/api/protected_tags_spec.rb2
-rw-r--r--spec/requests/api/pypi_packages_spec.rb15
-rw-r--r--spec/requests/api/release/links_spec.rb2
-rw-r--r--spec/requests/api/releases_spec.rb2
-rw-r--r--spec/requests/api/remote_mirrors_spec.rb6
-rw-r--r--spec/requests/api/repositories_spec.rb10
-rw-r--r--spec/requests/api/resource_access_tokens_spec.rb2
-rw-r--r--spec/requests/api/resource_label_events_spec.rb2
-rw-r--r--spec/requests/api/resource_milestone_events_spec.rb2
-rw-r--r--spec/requests/api/rpm_project_packages_spec.rb22
-rw-r--r--spec/requests/api/rubygem_packages_spec.rb8
-rw-r--r--spec/requests/api/search_spec.rb2
-rw-r--r--spec/requests/api/settings_spec.rb16
-rw-r--r--spec/requests/api/sidekiq_metrics_spec.rb2
-rw-r--r--spec/requests/api/snippet_repository_storage_moves_spec.rb2
-rw-r--r--spec/requests/api/snippets_spec.rb2
-rw-r--r--spec/requests/api/statistics_spec.rb2
-rw-r--r--spec/requests/api/submodules_spec.rb2
-rw-r--r--spec/requests/api/suggestions_spec.rb2
-rw-r--r--spec/requests/api/system_hooks_spec.rb2
-rw-r--r--spec/requests/api/tags_spec.rb58
-rw-r--r--spec/requests/api/task_completion_status_spec.rb72
-rw-r--r--spec/requests/api/templates_spec.rb2
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb10
-rw-r--r--spec/requests/api/terraform/state_spec.rb321
-rw-r--r--spec/requests/api/terraform/state_version_spec.rb2
-rw-r--r--spec/requests/api/todos_spec.rb20
-rw-r--r--spec/requests/api/topics_spec.rb2
-rw-r--r--spec/requests/api/unleash_spec.rb81
-rw-r--r--spec/requests/api/usage_data_non_sql_metrics_spec.rb2
-rw-r--r--spec/requests/api/usage_data_queries_spec.rb4
-rw-r--r--spec/requests/api/usage_data_spec.rb2
-rw-r--r--spec/requests/api/user_counts_spec.rb2
-rw-r--r--spec/requests/api/users_preferences_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb25
-rw-r--r--spec/requests/api/v3/github_spec.rb2
-rw-r--r--spec/requests/api/wikis_spec.rb2
-rw-r--r--spec/requests/concerns/planning_hierarchy_spec.rb2
-rw-r--r--spec/requests/content_security_policy_spec.rb2
-rw-r--r--spec/requests/dashboard/projects_controller_spec.rb2
-rw-r--r--spec/requests/dashboard_controller_spec.rb2
-rw-r--r--spec/requests/git_http_spec.rb2
-rw-r--r--spec/requests/groups/autocomplete_sources_spec.rb2
-rw-r--r--spec/requests/groups/clusters/integrations_controller_spec.rb2
-rw-r--r--spec/requests/groups/crm/contacts_controller_spec.rb2
-rw-r--r--spec/requests/groups/crm/organizations_controller_spec.rb2
-rw-r--r--spec/requests/groups/deploy_tokens_controller_spec.rb2
-rw-r--r--spec/requests/groups/email_campaigns_controller_spec.rb2
-rw-r--r--spec/requests/groups/harbor/artifacts_controller_spec.rb2
-rw-r--r--spec/requests/groups/harbor/repositories_controller_spec.rb2
-rw-r--r--spec/requests/groups/harbor/tags_controller_spec.rb2
-rw-r--r--spec/requests/groups/milestones_controller_spec.rb2
-rw-r--r--spec/requests/groups/observability_controller_spec.rb96
-rw-r--r--spec/requests/groups/registry/repositories_controller_spec.rb2
-rw-r--r--spec/requests/groups/settings/access_tokens_controller_spec.rb2
-rw-r--r--spec/requests/groups/settings/applications_controller_spec.rb2
-rw-r--r--spec/requests/groups/usage_quotas_controller_spec.rb48
-rw-r--r--spec/requests/groups_controller_spec.rb2
-rw-r--r--spec/requests/health_controller_spec.rb2
-rw-r--r--spec/requests/ide_controller_spec.rb27
-rw-r--r--spec/requests/import/github_groups_controller_spec.rb2
-rw-r--r--spec/requests/import/gitlab_groups_controller_spec.rb2
-rw-r--r--spec/requests/import/gitlab_projects_controller_spec.rb2
-rw-r--r--spec/requests/import/url_controller_spec.rb2
-rw-r--r--spec/requests/jira_authorizations_spec.rb2
-rw-r--r--spec/requests/jira_connect/cors_preflight_checks_controller_spec.rb59
-rw-r--r--spec/requests/jira_connect/installations_controller_spec.rb83
-rw-r--r--spec/requests/jira_connect/oauth_application_ids_controller_spec.rb29
-rw-r--r--spec/requests/jira_connect/oauth_callbacks_controller_spec.rb2
-rw-r--r--spec/requests/jira_connect/public_keys_controller_spec.rb14
-rw-r--r--spec/requests/jira_connect/subscriptions_controller_spec.rb65
-rw-r--r--spec/requests/jira_connect/users_controller_spec.rb2
-rw-r--r--spec/requests/jira_routing_spec.rb2
-rw-r--r--spec/requests/jwks_controller_spec.rb2
-rw-r--r--spec/requests/jwt_controller_spec.rb2
-rw-r--r--spec/requests/lfs_http_spec.rb89
-rw-r--r--spec/requests/lfs_locks_api_spec.rb2
-rw-r--r--spec/requests/mailgun/webhooks_controller_spec.rb2
-rw-r--r--spec/requests/oauth/applications_controller_spec.rb2
-rw-r--r--spec/requests/oauth/authorizations_controller_spec.rb2
-rw-r--r--spec/requests/oauth/tokens_controller_spec.rb2
-rw-r--r--spec/requests/oauth_tokens_spec.rb2
-rw-r--r--spec/requests/openid_connect_spec.rb2
-rw-r--r--spec/requests/profiles/notifications_controller_spec.rb2
-rw-r--r--spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb2
-rw-r--r--spec/requests/projects/cluster_agents_controller_spec.rb2
-rw-r--r--spec/requests/projects/clusters/integrations_controller_spec.rb2
-rw-r--r--spec/requests/projects/commits_controller_spec.rb2
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb2
-rw-r--r--spec/requests/projects/environments_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/configuration_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/databases_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/deployments_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/service_accounts_controller_spec.rb2
-rw-r--r--spec/requests/projects/harbor/artifacts_controller_spec.rb2
-rw-r--r--spec/requests/projects/harbor/repositories_controller_spec.rb2
-rw-r--r--spec/requests/projects/harbor/tags_controller_spec.rb2
-rw-r--r--spec/requests/projects/hook_logs_controller_spec.rb2
-rw-r--r--spec/requests/projects/incident_management/pagerduty_incidents_spec.rb2
-rw-r--r--spec/requests/projects/incident_management/timeline_events_spec.rb2
-rw-r--r--spec/requests/projects/integrations/shimos_controller_spec.rb2
-rw-r--r--spec/requests/projects/issue_links_controller_spec.rb2
-rw-r--r--spec/requests/projects/issues/discussions_spec.rb2
-rw-r--r--spec/requests/projects/issues_controller_spec.rb26
-rw-r--r--spec/requests/projects/merge_requests/content_spec.rb2
-rw-r--r--spec/requests/projects/merge_requests/context_commit_diffs_spec.rb2
-rw-r--r--spec/requests/projects/merge_requests/creations_spec.rb14
-rw-r--r--spec/requests/projects/merge_requests/diffs_spec.rb16
-rw-r--r--spec/requests/projects/merge_requests_controller_spec.rb21
-rw-r--r--spec/requests/projects/merge_requests_discussions_spec.rb2
-rw-r--r--spec/requests/projects/merge_requests_spec.rb2
-rw-r--r--spec/requests/projects/metrics/dashboards/builder_spec.rb2
-rw-r--r--spec/requests/projects/metrics_dashboard_spec.rb2
-rw-r--r--spec/requests/projects/ml/candidates_controller_spec.rb69
-rw-r--r--spec/requests/projects/ml/experiments_controller_spec.rb10
-rw-r--r--spec/requests/projects/network_controller_spec.rb57
-rw-r--r--spec/requests/projects/noteable_notes_spec.rb2
-rw-r--r--spec/requests/projects/packages/package_files_controller_spec.rb2
-rw-r--r--spec/requests/projects/pipelines_controller_spec.rb2
-rw-r--r--spec/requests/projects/redirect_controller_spec.rb2
-rw-r--r--spec/requests/projects/releases_controller_spec.rb2
-rw-r--r--spec/requests/projects/settings/access_tokens_controller_spec.rb2
-rw-r--r--spec/requests/projects/settings/integration_hook_logs_controller_spec.rb2
-rw-r--r--spec/requests/projects/settings/packages_and_registries_controller_spec.rb2
-rw-r--r--spec/requests/projects/tags_controller_spec.rb2
-rw-r--r--spec/requests/projects/uploads_spec.rb2
-rw-r--r--spec/requests/projects/usage_quotas_spec.rb2
-rw-r--r--spec/requests/projects/work_items_spec.rb2
-rw-r--r--spec/requests/projects_controller_spec.rb2
-rw-r--r--spec/requests/pwa_controller_spec.rb19
-rw-r--r--spec/requests/rack_attack_global_spec.rb3
-rw-r--r--spec/requests/recursive_webhook_detection_spec.rb3
-rw-r--r--spec/requests/robots_txt_spec.rb2
-rw-r--r--spec/requests/runner_setup_controller_spec.rb2
-rw-r--r--spec/requests/sandbox_controller_spec.rb2
-rw-r--r--spec/requests/search_controller_spec.rb94
-rw-r--r--spec/requests/self_monitoring_project_spec.rb2
-rw-r--r--spec/requests/sessions_spec.rb2
-rw-r--r--spec/requests/terraform/services_controller_spec.rb2
-rw-r--r--spec/requests/user_activity_spec.rb2
-rw-r--r--spec/requests/user_avatar_spec.rb2
-rw-r--r--spec/requests/user_sends_malformed_strings_spec.rb2
-rw-r--r--spec/requests/user_spoofs_ip_spec.rb2
-rw-r--r--spec/requests/users/group_callouts_spec.rb2
-rw-r--r--spec/requests/users/project_callouts_spec.rb2
-rw-r--r--spec/requests/users_controller_spec.rb2
-rw-r--r--spec/requests/verifies_with_email_spec.rb25
-rw-r--r--spec/requests/web_ide/remote_ide_controller_spec.rb145
-rw-r--r--spec/requests/whats_new_controller_spec.rb2
-rw-r--r--spec/routing/group_routing_spec.rb4
-rw-r--r--spec/routing/project_routing_spec.rb5
-rw-r--r--spec/routing/user_routing_spec.rb29
-rw-r--r--spec/routing/web_ide_routing_spec.rb22
-rw-r--r--spec/rubocop/cop/feature_flag_usage_spec.rb55
-rw-r--r--spec/rubocop/cop/filename_length_spec.rb1
-rw-r--r--spec/rubocop/cop/gitlab/feature_available_usage_spec.rb2
-rw-r--r--spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb2
-rw-r--r--spec/rubocop/cop/gitlab/strong_memoize_attr_spec.rb75
-rw-r--r--spec/rubocop/cop/graphql/descriptions_spec.rb241
-rw-r--r--spec/rubocop/cop/migration/add_column_with_default_spec.rb33
-rw-r--r--spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb18
-rw-r--r--spec/rubocop/cop/migration/batch_migrations_post_only_spec.rb84
-rw-r--r--spec/rubocop/cop/migration/prevent_strings_spec.rb12
-rw-r--r--spec/rubocop/cop/migration/versioned_migration_class_spec.rb10
-rw-r--r--spec/rubocop/cop/performance/readlines_each_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/avoid_test_prof_spec.rb49
-rw-r--r--spec/rubocop/cop/rspec/timecop_freeze_spec.rb28
-rw-r--r--spec/rubocop/cop/rspec/timecop_travel_spec.rb28
-rw-r--r--spec/rubocop/cop/user_admin_spec.rb2
-rw-r--r--spec/rubocop/formatter/graceful_formatter_spec.rb4
-rw-r--r--spec/rubocop/support_workaround.rb33
-rw-r--r--spec/rubocop_spec_helper.rb4
-rw-r--r--spec/scripts/lib/glfm/shared_spec.rb6
-rw-r--r--spec/scripts/lib/glfm/update_specification_spec.rb197
-rw-r--r--spec/scripts/trigger-build_spec.rb22
-rw-r--r--spec/serializers/ci/group_variable_entity_spec.rb6
-rw-r--r--spec/serializers/ci/variable_entity_spec.rb6
-rw-r--r--spec/serializers/deploy_keys/deploy_key_entity_spec.rb3
-rw-r--r--spec/serializers/entity_date_helper_spec.rb6
-rw-r--r--spec/serializers/issue_entity_spec.rb20
-rw-r--r--spec/serializers/linked_project_issue_entity_spec.rb10
-rw-r--r--spec/serializers/member_entity_spec.rb22
-rw-r--r--spec/serializers/merge_request_poll_cached_widget_entity_spec.rb4
-rw-r--r--spec/serializers/merge_request_user_entity_spec.rb2
-rw-r--r--spec/serializers/pipeline_details_entity_spec.rb8
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb24
-rw-r--r--spec/serializers/prometheus_alert_entity_spec.rb4
-rw-r--r--spec/serializers/release_serializer_spec.rb11
-rw-r--r--spec/services/admin/set_feature_flag_service_spec.rb199
-rw-r--r--spec/services/bulk_imports/create_service_spec.rb118
-rw-r--r--spec/services/bulk_imports/file_download_service_spec.rb80
-rw-r--r--spec/services/chat_names/find_user_service_spec.rb20
-rw-r--r--spec/services/ci/append_build_trace_service_spec.rb32
-rw-r--r--spec/services/ci/create_downstream_pipeline_service_spec.rb237
-rw-r--r--spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/environment_spec.rb18
-rw-r--r--spec/services/ci/create_pipeline_service/logger_spec.rb43
-rw-r--r--spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/partitioning_spec.rb21
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb8
-rw-r--r--spec/services/ci/create_pipeline_service/scripts_spec.rb112
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb33
-rw-r--r--spec/services/ci/enqueue_job_service_spec.rb81
-rw-r--r--spec/services/ci/generate_kubeconfig_service_spec.rb110
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_schedules/calculate_next_run_service_spec.rb107
-rw-r--r--spec/services/ci/pipeline_trigger_service_spec.rb3
-rw-r--r--spec/services/ci/pipelines/add_job_service_spec.rb4
-rw-r--r--spec/services/ci/process_build_service_spec.rb72
-rw-r--r--spec/services/ci/reset_skipped_jobs_service_spec.rb (renamed from spec/services/ci/after_requeue_job_service_spec.rb)18
-rw-r--r--spec/services/ci/retry_job_service_spec.rb45
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb5
-rw-r--r--spec/services/ci/runners/assign_runner_service_spec.rb2
-rw-r--r--spec/services/ci/runners/bulk_delete_runners_service_spec.rb2
-rw-r--r--spec/services/ci/runners/process_runner_version_update_service_spec.rb2
-rw-r--r--spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb2
-rw-r--r--spec/services/ci/runners/register_runner_service_spec.rb2
-rw-r--r--spec/services/ci/runners/reset_registration_token_service_spec.rb2
-rw-r--r--spec/services/ci/runners/set_runner_associated_projects_service_spec.rb2
-rw-r--r--spec/services/ci/runners/unassign_runner_service_spec.rb2
-rw-r--r--spec/services/ci/runners/unregister_runner_service_spec.rb2
-rw-r--r--spec/services/ci/runners/update_runner_service_spec.rb2
-rw-r--r--spec/services/ci/test_failure_history_service_spec.rb37
-rw-r--r--spec/services/ci/track_failed_build_service_spec.rb23
-rw-r--r--spec/services/ci/unlock_artifacts_service_spec.rb27
-rw-r--r--spec/services/clusters/agents/filter_authorizations_service_spec.rb100
-rw-r--r--spec/services/clusters/agents/refresh_authorization_service_spec.rb10
-rw-r--r--spec/services/clusters/applications/install_service_spec.rb80
-rw-r--r--spec/services/clusters/applications/prometheus_config_service_spec.rb162
-rw-r--r--spec/services/clusters/applications/upgrade_service_spec.rb80
-rw-r--r--spec/services/database/consistency_check_service_spec.rb2
-rw-r--r--spec/services/deployments/update_environment_service_spec.rb2
-rw-r--r--spec/services/environments/create_for_build_service_spec.rb8
-rw-r--r--spec/services/environments/stop_service_spec.rb5
-rw-r--r--spec/services/event_create_service_spec.rb109
-rw-r--r--spec/services/feature_flags/hook_service_spec.rb8
-rw-r--r--spec/services/google_cloud/fetch_google_ip_list_service_spec.rb4
-rw-r--r--spec/services/groups/destroy_service_spec.rb20
-rw-r--r--spec/services/groups/import_export/import_service_spec.rb42
-rw-r--r--spec/services/import/bitbucket_server_service_spec.rb19
-rw-r--r--spec/services/import/github/gists_import_service_spec.rb47
-rw-r--r--spec/services/import/github_service_spec.rb23
-rw-r--r--spec/services/incident_management/incidents/create_service_spec.rb24
-rw-r--r--spec/services/incident_management/link_alerts/create_service_spec.rb98
-rw-r--r--spec/services/incident_management/link_alerts/destroy_service_spec.rb88
-rw-r--r--spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb14
-rw-r--r--spec/services/incident_management/pager_duty/process_webhook_service_spec.rb2
-rw-r--r--spec/services/incident_management/timeline_events/create_service_spec.rb18
-rw-r--r--spec/services/incident_management/timeline_events/destroy_service_spec.rb9
-rw-r--r--spec/services/incident_management/timeline_events/update_service_spec.rb168
-rw-r--r--spec/services/issuable/discussions_list_service_spec.rb13
-rw-r--r--spec/services/issue_links/create_service_spec.rb8
-rw-r--r--spec/services/issue_links/destroy_service_spec.rb8
-rw-r--r--spec/services/issues/close_service_spec.rb44
-rw-r--r--spec/services/issues/create_service_spec.rb27
-rw-r--r--spec/services/issues/move_service_spec.rb21
-rw-r--r--spec/services/issues/reopen_service_spec.rb8
-rw-r--r--spec/services/issues/update_service_spec.rb31
-rw-r--r--spec/services/issues/zoom_link_service_spec.rb8
-rw-r--r--spec/services/jira_connect/create_asymmetric_jwt_service_spec.rb27
-rw-r--r--spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb154
-rw-r--r--spec/services/jira_connect_installations/update_service_spec.rb186
-rw-r--r--spec/services/markup/rendering_service_spec.rb51
-rw-r--r--spec/services/merge_requests/after_create_service_spec.rb2
-rw-r--r--spec/services/merge_requests/approval_service_spec.rb8
-rw-r--r--spec/services/merge_requests/build_service_spec.rb99
-rw-r--r--spec/services/merge_requests/create_pipeline_service_spec.rb21
-rw-r--r--spec/services/merge_requests/remove_approval_service_spec.rb8
-rw-r--r--spec/services/ml/experiment_tracking/candidate_repository_spec.rb78
-rw-r--r--spec/services/ml/experiment_tracking/experiment_repository_spec.rb39
-rw-r--r--spec/services/notes/create_service_spec.rb8
-rw-r--r--spec/services/notification_service_spec.rb10
-rw-r--r--spec/services/packages/debian/process_changes_service_spec.rb4
-rw-r--r--spec/services/packages/debian/process_package_file_service_spec.rb161
-rw-r--r--spec/services/pages_domains/retry_acme_order_service_spec.rb36
-rw-r--r--spec/services/personal_access_tokens/revoke_service_spec.rb44
-rw-r--r--spec/services/projects/after_rename_service_spec.rb29
-rw-r--r--spec/services/projects/container_repository/destroy_service_spec.rb51
-rw-r--r--spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb16
-rw-r--r--spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb8
-rw-r--r--spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb10
-rw-r--r--spec/services/projects/create_service_spec.rb35
-rw-r--r--spec/services/projects/destroy_service_spec.rb33
-rw-r--r--spec/services/projects/download_service_spec.rb4
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb17
-rw-r--r--spec/services/projects/import_export/parallel_export_service_spec.rb98
-rw-r--r--spec/services/projects/import_service_spec.rb20
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb64
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb16
-rw-r--r--spec/services/projects/lfs_pointers/lfs_import_service_spec.rb14
-rw-r--r--spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb64
-rw-r--r--spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb3
-rw-r--r--spec/services/projects/transfer_service_spec.rb30
-rw-r--r--spec/services/projects/update_pages_service_spec.rb7
-rw-r--r--spec/services/projects/update_service_spec.rb39
-rw-r--r--spec/services/protected_branches/api_service_spec.rb69
-rw-r--r--spec/services/protected_branches/cache_service_spec.rb186
-rw-r--r--spec/services/protected_branches/create_service_spec.rb97
-rw-r--r--spec/services/protected_branches/destroy_service_spec.rb59
-rw-r--r--spec/services/protected_branches/update_service_spec.rb74
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb18
-rw-r--r--spec/services/repositories/housekeeping_service_spec.rb2
-rw-r--r--spec/services/search_service_spec.rb2
-rw-r--r--spec/services/security/merge_reports_service_spec.rb26
-rw-r--r--spec/services/service_ping/submit_service_ping_service_spec.rb2
-rw-r--r--spec/services/timelogs/create_service_spec.rb6
-rw-r--r--spec/services/todo_service_spec.rb99
-rw-r--r--spec/services/users/assigned_issues_count_service_spec.rb57
-rw-r--r--spec/services/users/keys_count_service_spec.rb6
-rw-r--r--spec/services/users/migrate_records_to_ghost_user_service_spec.rb14
-rw-r--r--spec/services/users/registrations_build_service_spec.rb4
-rw-r--r--spec/services/web_hooks/log_execution_service_spec.rb39
-rw-r--r--spec/services/work_items/create_and_link_service_spec.rb4
-rw-r--r--spec/services/work_items/create_service_spec.rb4
-rw-r--r--spec/services/work_items/parent_links/create_service_spec.rb6
-rw-r--r--spec/services/work_items/update_service_spec.rb2
-rw-r--r--spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb6
-rw-r--r--spec/sidekiq_cluster/sidekiq_cluster_spec.rb23
-rw-r--r--spec/simplecov_env.rb14
-rw-r--r--spec/spec_helper.rb5
-rw-r--r--spec/support/atlassian/jira_connect/schemata.rb2
-rw-r--r--spec/support/banzai/filter_timeout_shared_examples.rb37
-rw-r--r--spec/support/before_all_adapter.rb51
-rw-r--r--spec/support/capybara.rb20
-rw-r--r--spec/support/counter_attribute.rb7
-rw-r--r--spec/support/cycle_analytics_helpers/test_generation.rb28
-rw-r--r--spec/support/database/query_recorder.rb12
-rw-r--r--spec/support/db_cleaner.rb4
-rw-r--r--spec/support/finder_collection_allowlist.yml4
-rw-r--r--spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb2
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci.yml3
-rw-r--r--spec/support/helpers/batch_destroy_dependent_associations_helper.rb13
-rw-r--r--spec/support/helpers/ci/partitioning_helpers.rb11
-rw-r--r--spec/support/helpers/content_security_policy_helpers.rb10
-rw-r--r--spec/support/helpers/cookie_helper.rb6
-rw-r--r--spec/support/helpers/countries_controller_test_helper.rb9
-rw-r--r--spec/support/helpers/doc_url_helper.rb2
-rw-r--r--spec/support/helpers/features/branches_helpers.rb10
-rw-r--r--spec/support/helpers/features/invite_members_modal_helper.rb29
-rw-r--r--spec/support/helpers/features/runners_helpers.rb2
-rw-r--r--spec/support/helpers/gitaly_setup.rb2
-rw-r--r--spec/support/helpers/graphql_helpers.rb4
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb4
-rw-r--r--spec/support/helpers/listbox_input_helper.rb18
-rw-r--r--spec/support/helpers/migrations_helpers/work_item_types_helper.rb21
-rw-r--r--spec/support/helpers/project_template_test_helper.rb16
-rw-r--r--spec/support/helpers/repo_helpers.rb24
-rw-r--r--spec/support/helpers/search_helpers.rb2
-rw-r--r--spec/support/helpers/service_desk_helper.rb9
-rw-r--r--spec/support/helpers/smime_helper.rb2
-rw-r--r--spec/support/helpers/stub_configuration.rb11
-rw-r--r--spec/support/helpers/stub_object_storage.rb3
-rw-r--r--spec/support/helpers/stub_snowplow.rb2
-rw-r--r--spec/support/helpers/test_env.rb26
-rw-r--r--spec/support/helpers/usage_data_helpers.rb230
-rw-r--r--spec/support/helpers/workhorse_helpers.rb8
-rw-r--r--spec/support/import_export/common_util.rb4
-rw-r--r--spec/support/import_export/export_file_helper.rb4
-rw-r--r--spec/support/matchers/exceed_query_limit.rb18
-rw-r--r--spec/support/memory_instrumentation_helper.rb7
-rw-r--r--spec/support/migration.rb8
-rw-r--r--spec/support/migrations_helpers/vulnerabilities_findings_helper.rb8
-rw-r--r--spec/support/models/ci/partitioning_testing/cascade_check.rb7
-rw-r--r--spec/support/models/ci/partitioning_testing/schema_helpers.rb6
-rw-r--r--spec/support/patches/rspec_mocks_prepended_methods.rb2
-rw-r--r--spec/support/prometheus/additional_metrics_shared_examples.rb12
-rw-r--r--spec/support/redis/redis_shared_examples.rb55
-rw-r--r--spec/support/rspec.rb11
-rw-r--r--spec/support/rspec_order_todo.yml192
-rw-r--r--spec/support/shared_contexts/disable_user_tracking.rb10
-rw-r--r--spec/support/shared_contexts/email_shared_context.rb10
-rw-r--r--spec/support/shared_contexts/models/ci/job_token_scope.rb21
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb38
-rw-r--r--spec/support/shared_contexts/rack_attack_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb1
-rw-r--r--spec/support/shared_contexts/rubocop_default_rspec_language_config_context.rb32
-rw-r--r--spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb2
-rw-r--r--spec/support/shared_examples/boards/destroy_service_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/ci/log_downstream_pipeline_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/ci/retryable_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/ci/stuck_builds_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb10
-rw-r--r--spec/support/shared_examples/controllers/variables_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/csp.rb15
-rw-r--r--spec/support/shared_examples/features/container_registry_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb4
-rw-r--r--spec/support/shared_examples/features/inviting_members_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/features/reportable_note_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/runners_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/sidebar/sidebar_milestone_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb84
-rw-r--r--spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/graphql/label_fields.rb4
-rw-r--r--spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/notes_creation_shared_examples.rb56
-rw-r--r--spec/support/shared_examples/initializers/uses_gitlab_url_blocker_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb8
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb49
-rw-r--r--spec/support/shared_examples/mailers/notify_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb136
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/concerns/signature_type_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/label_note_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/update_highest_role_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/observability/csp_shared_examples.rb123
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb513
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb38
-rw-r--r--spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/requests/api/notes_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb165
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/security_training_providers_importer.rb2
-rw-r--r--spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/alert_management_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/approval_state_updated_trigger_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/incident_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/issuable/update_service_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb47
-rw-r--r--spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/timelogs/create_service_shared_examples.rb47
-rw-r--r--spec/support/shared_examples/services/users/build_service_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/work_item_base_types_importer.rb6
-rw-r--r--spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb59
-rw-r--r--spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb (renamed from spec/workers/database/batched_background_migration/execution_worker_spec.rb)86
-rw-r--r--spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb227
-rw-r--r--spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/workers/update_repository_move_shared_examples.rb4
-rw-r--r--spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb2
-rw-r--r--spec/tasks/gitlab/db/lock_writes_rake_spec.rb46
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb10
-rw-r--r--spec/tasks/gitlab/feature_categories_rake_spec.rb51
-rw-r--r--spec/tasks/gitlab/lfs/migrate_rake_spec.rb4
-rw-r--r--spec/tasks/gitlab/refresh_project_statistics_build_artifacts_size_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb19
-rw-r--r--spec/tasks/gitlab/update_templates_rake_spec.rb8
-rw-r--r--spec/tasks/gitlab/usage_data_rake_spec.rb11
-rw-r--r--spec/tooling/danger/feature_flag_spec.rb2
-rw-r--r--spec/tooling/danger/product_intelligence_spec.rb74
-rw-r--r--spec/tooling/danger/project_helper_spec.rb2
-rw-r--r--spec/tooling/danger/specs_spec.rb66
-rw-r--r--spec/tooling/danger/stable_branch_spec.rb169
-rw-r--r--spec/tooling/danger/user_types_spec.rb56
-rw-r--r--spec/tooling/docs/deprecation_handling_spec.rb4
-rw-r--r--spec/tooling/fixtures/metrics/sample_instrumentation_metric.rb15
-rw-r--r--spec/tooling/quality/test_level_spec.rb4
-rw-r--r--spec/uploaders/ci/secure_file_uploader_spec.rb6
-rw-r--r--spec/uploaders/external_diff_uploader_spec.rb25
-rw-r--r--spec/uploaders/file_mover_spec.rb6
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb15
-rw-r--r--spec/uploaders/lfs_object_uploader_spec.rb24
-rw-r--r--spec/uploaders/packages/composer/cache_uploader_spec.rb2
-rw-r--r--spec/uploaders/packages/debian/component_file_uploader_spec.rb2
-rw-r--r--spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb2
-rw-r--r--spec/uploaders/packages/package_file_uploader_spec.rb2
-rw-r--r--spec/uploaders/packages/rpm/repository_file_uploader_spec.rb2
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb8
-rw-r--r--spec/uploaders/terraform/state_uploader_spec.rb6
-rw-r--r--spec/uploaders/workers/object_storage/background_move_worker_spec.rb116
-rw-r--r--spec/validators/iso8601_date_validator_spec.rb32
-rw-r--r--spec/views/admin/application_settings/_ci_cd.html.haml_spec.rb8
-rw-r--r--spec/views/admin/application_settings/_repository_check.html.haml_spec.rb27
-rw-r--r--spec/views/admin/application_settings/general.html.haml_spec.rb12
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb4
-rw-r--r--spec/views/help/index.html.haml_spec.rb2
-rw-r--r--spec/views/import/gitlab_projects/new.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb40
-rw-r--r--spec/views/profiles/keys/_form.html.haml_spec.rb5
-rw-r--r--spec/views/profiles/keys/_key.html.haml_spec.rb20
-rw-r--r--spec/views/profiles/keys/_key_details.html.haml_spec.rb32
-rw-r--r--spec/views/projects/_files.html.haml_spec.rb73
-rw-r--r--spec/views/projects/_flash_messages.html.haml_spec.rb4
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb36
-rw-r--r--spec/views/projects/commit/show.html.haml_spec.rb42
-rw-r--r--spec/views/projects/issues/_related_branches.html.haml_spec.rb11
-rw-r--r--spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb6
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb11
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb2
-rw-r--r--spec/views/search/_results.html.haml_spec.rb66
-rw-r--r--spec/views/search/show.html.haml_spec.rb27
-rw-r--r--spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb20
-rw-r--r--spec/views/shared/ssh_keys/_key_delete.html.haml_spec.rb (renamed from spec/views/shared/ssh_keys/_key_details.html.haml_spec.rb)12
-rw-r--r--spec/workers/bulk_import_worker_spec.rb2
-rw-r--r--spec/workers/bulk_imports/entity_worker_spec.rb2
-rw-r--r--spec/workers/bulk_imports/export_request_worker_spec.rb90
-rw-r--r--spec/workers/bulk_imports/pipeline_worker_spec.rb236
-rw-r--r--spec/workers/ci/create_downstream_pipeline_worker_spec.rb43
-rw-r--r--spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb27
-rw-r--r--spec/workers/ci/runners/process_runner_version_update_worker_spec.rb2
-rw-r--r--spec/workers/ci/runners/reconcile_existing_runner_versions_cron_worker_spec.rb2
-rw-r--r--spec/workers/concerns/gitlab/github_import/object_importer_spec.rb37
-rw-r--r--spec/workers/concerns/waitable_worker_spec.rb43
-rw-r--r--spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb3
-rw-r--r--spec/workers/container_registry/cleanup_worker_spec.rb14
-rw-r--r--spec/workers/container_registry/migration/guard_worker_spec.rb16
-rw-r--r--spec/workers/database/batched_background_migration/ci_execution_worker_spec.rb9
-rw-r--r--spec/workers/database/batched_background_migration/main_execution_worker_spec.rb9
-rw-r--r--spec/workers/delete_container_repository_worker_spec.rb108
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb9
-rw-r--r--spec/workers/flush_counter_increments_worker_spec.rb21
-rw-r--r--spec/workers/gitlab/export/prune_project_export_jobs_worker_spec.rb52
-rw-r--r--spec/workers/gitlab/github_gists_import/finish_import_worker_spec.rb51
-rw-r--r--spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb94
-rw-r--r--spec/workers/gitlab/github_gists_import/start_import_worker_spec.rb110
-rw-r--r--spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb1
-rw-r--r--spec/workers/gitlab/jira_import/import_issue_worker_spec.rb3
-rw-r--r--spec/workers/gitlab_shell_worker_spec.rb26
-rw-r--r--spec/workers/incident_management/close_incident_worker_spec.rb6
-rw-r--r--spec/workers/incident_management/pager_duty/process_incident_worker_spec.rb8
-rw-r--r--spec/workers/issuable_export_csv_worker_spec.rb4
-rw-r--r--spec/workers/jira_connect/forward_event_worker_spec.rb6
-rw-r--r--spec/workers/jira_connect/send_uninstalled_hook_worker_spec.rb29
-rw-r--r--spec/workers/mail_scheduler/notification_service_worker_spec.rb30
-rw-r--r--spec/workers/merge_requests/delete_branch_worker_spec.rb65
-rw-r--r--spec/workers/merge_requests/delete_source_branch_worker_spec.rb23
-rw-r--r--spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb13
-rw-r--r--spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb4
-rw-r--r--spec/workers/namespaces/process_sync_events_worker_spec.rb6
-rw-r--r--spec/workers/namespaces/root_statistics_worker_spec.rb1
-rw-r--r--spec/workers/namespaces/schedule_aggregation_worker_spec.rb4
-rw-r--r--spec/workers/packages/debian/process_changes_worker_spec.rb20
-rw-r--r--spec/workers/packages/debian/process_package_file_worker_spec.rb138
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb26
-rw-r--r--spec/workers/post_receive_spec.rb39
-rw-r--r--spec/workers/process_commit_worker_spec.rb2
-rw-r--r--spec/workers/projects/delete_branch_worker_spec.rb112
-rw-r--r--spec/workers/projects/import_export/parallel_project_export_worker_spec.rb60
-rw-r--r--spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb4
-rw-r--r--spec/workers/projects/process_sync_events_worker_spec.rb4
-rw-r--r--spec/workers/releases/create_evidence_worker_spec.rb4
-rw-r--r--spec/workers/releases/manage_evidence_worker_spec.rb4
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb10
-rw-r--r--spec/workers/run_pipeline_schedule_worker_spec.rb10
-rw-r--r--spec/workers/tasks_to_be_done/create_worker_spec.rb4
-rw-r--r--spec/workers/update_highest_role_worker_spec.rb4
3405 files changed, 50677 insertions, 25008 deletions
diff --git a/spec/bin/audit_event_type_spec.rb b/spec/bin/audit_event_type_spec.rb
index d4b1ebf08de..e23d365f68f 100644
--- a/spec/bin/audit_event_type_spec.rb
+++ b/spec/bin/audit_event_type_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'bin/audit-event-type' do
using RSpec::Parameterized::TableSyntax
describe AuditEventTypeCreator do
- let(:argv) { %w[test_audit_event -d test -g govern::compliance -s -t -i https://url -m http://url] }
+ let(:argv) { %w[test_audit_event -d test -c compliance_management -s -t -i https://url -m http://url] }
let(:options) { AuditEventTypeOptionParser.parse(argv) }
let(:creator) { described_class.new(options) }
let(:existing_audit_event_types) do
@@ -71,8 +71,8 @@ RSpec.describe 'bin/audit-event-type' do
:force | %w[foo --force] | true
:description | %w[foo -d desc] | 'desc'
:description | %w[foo --description desc] | 'desc'
- :group | %w[foo -g govern::compliance] | 'govern::compliance'
- :group | %w[foo --group govern::compliance] | 'govern::compliance'
+ :feature_category | %w[foo -c audit_events] | 'audit_events'
+ :feature_category | %w[foo --feature-category audit_events] | 'audit_events'
:milestone | %w[foo -M 15.6] | '15.6'
:milestone | %w[foo --milestone 15.6] | '15.6'
:saved_to_database | %w[foo -s] | true
@@ -141,27 +141,27 @@ RSpec.describe 'bin/audit-event-type' do
end
end
- describe '.read_group' do
- let(:group) { 'govern::compliance' }
+ describe '.read_feature_category' do
+ let(:feature_category) { 'compliance_management' }
- it 'reads group from stdin' do
- expect(Readline).to receive(:readline).and_return(group)
+ it 'reads feature_category from stdin' do
+ expect(Readline).to receive(:readline).and_return(feature_category)
expect do
- expect(described_class.read_group).to eq('govern::compliance')
- end.to output(/Specify the group introducing the audit event type, like `govern::compliance`:/).to_stdout
+ expect(described_class.read_feature_category).to eq('compliance_management')
+ end.to output(/Specify the feature category of this audit event, like `compliance_management`:/).to_stdout
end
- context 'when group is empty' do
- let(:group) { '' }
+ context 'when feature category is empty' do
+ let(:feature_category) { '' }
it 'shows error message and retries' do
- expect(Readline).to receive(:readline).and_return(group)
+ expect(Readline).to receive(:readline).and_return(feature_category)
expect(Readline).to receive(:readline).and_raise('EOF')
expect do
- expect { described_class.read_group }.to raise_error(/EOF/)
- end.to output(/Specify the group introducing the audit event type, like `govern::compliance`:/)
- .to_stdout.and output(/group is a required field/).to_stderr
+ expect { described_class.read_feature_category }.to raise_error(/EOF/)
+ end.to output(/Specify the feature category of this audit event, like `compliance_management`:/)
+ .to_stdout.and output(/feature_category is a required field/).to_stderr
end
end
end
@@ -281,11 +281,11 @@ RSpec.describe 'bin/audit-event-type' do
describe '.read_milestone' do
before do
- allow(File).to receive(:read).with('VERSION').and_return('15.6.0-pre')
allow(File).to receive(:read).and_call_original
end
it 'returns the correct milestone from the VERSION file' do
+ expect(File).to receive(:read).with('VERSION').and_return('15.6.0-pre')
expect(described_class.read_milestone).to eq('15.6')
end
end
diff --git a/spec/bin/feature_flag_spec.rb b/spec/bin/feature_flag_spec.rb
index 03f5ac135f7..cce103965d3 100644
--- a/spec/bin/feature_flag_spec.rb
+++ b/spec/bin/feature_flag_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'bin/feature-flag' do
using RSpec::Parameterized::TableSyntax
describe FeatureFlagCreator do
- let(:argv) { %w[feature-flag-name -t development -g group::memory -i https://url -m http://url] }
+ let(:argv) { %w[feature-flag-name -t development -g group::geo -i https://url -m http://url] }
let(:options) { FeatureFlagOptionParser.parse(argv) }
let(:creator) { described_class.new(options) }
let(:existing_flags) do
@@ -81,8 +81,8 @@ RSpec.describe 'bin/feature-flag' do
:type | %w[foo --type development] | :development
:type | %w[foo -t invalid] | nil
:type | %w[foo --type invalid] | nil
- :group | %w[foo -g group::memory] | 'group::memory'
- :group | %w[foo --group group::memory] | 'group::memory'
+ :group | %w[foo -g group::geo] | 'group::geo'
+ :group | %w[foo --group group::geo] | 'group::geo'
:group | %w[foo -g invalid] | nil
:group | %w[foo --group invalid] | nil
end
@@ -178,12 +178,12 @@ RSpec.describe 'bin/feature-flag' do
end
describe '.read_group' do
- let(:group) { 'group::memory' }
+ let(:group) { 'group::geo' }
it 'reads type from stdin' do
expect(Readline).to receive(:readline).and_return(group)
expect do
- expect(described_class.read_group).to eq('group::memory')
+ expect(described_class.read_group).to eq('group::geo')
end.to output(/Specify the group introducing the feature flag/).to_stdout
end
diff --git a/spec/commands/sidekiq_cluster/cli_spec.rb b/spec/commands/sidekiq_cluster/cli_spec.rb
index 4d1a07a6a75..c2ea9455de6 100644
--- a/spec/commands/sidekiq_cluster/cli_spec.rb
+++ b/spec/commands/sidekiq_cluster/cli_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
let(:cli) { described_class.new('/dev/null') }
let(:timeout) { Gitlab::SidekiqCluster::DEFAULT_SOFT_TIMEOUT_SECONDS }
let(:default_options) do
- { env: 'test', directory: Dir.pwd, max_concurrency: 50, min_concurrency: 0, dryrun: false, timeout: timeout }
+ { env: 'test', directory: Dir.pwd, max_concurrency: 20, min_concurrency: 0, dryrun: false, timeout: timeout }
end
let(:sidekiq_exporter_enabled) { false }
@@ -245,9 +245,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
it 'expands multiple queue groups correctly' do
expected_workers =
if Gitlab.ee?
- [%w[chat_notification], %w[project_export projects_import_export_relation_export project_template_export]]
+ [%w[chat_notification], %w[project_export projects_import_export_parallel_project_export projects_import_export_relation_export project_template_export]]
else
- [%w[chat_notification], %w[project_export projects_import_export_relation_export]]
+ [%w[chat_notification], %w[project_export projects_import_export_parallel_project_export projects_import_export_relation_export]]
end
expect(Gitlab::SidekiqCluster)
@@ -299,11 +299,11 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
end
context 'starting the server' do
- context 'without --dryrun' do
- before do
- allow(Gitlab::SidekiqCluster).to receive(:start).and_return([])
- end
+ before do
+ allow(Gitlab::SidekiqCluster).to receive(:start).and_return([])
+ end
+ context 'without --dryrun' do
it 'wipes the metrics directory before starting workers' do
expect(metrics_cleanup_service).to receive(:execute).ordered
expect(Gitlab::SidekiqCluster).to receive(:start).ordered.and_return([])
@@ -403,9 +403,42 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
let(:sidekiq_exporter_enabled) { true }
let(:metrics_server_pid) { 99 }
let(:sidekiq_worker_pids) { [2, 42] }
+ let(:waiter_threads) { [instance_double('Process::Waiter'), instance_double('Process::Waiter')] }
+ let(:process_status) { instance_double('Process::Status') }
before do
- allow(Gitlab::SidekiqCluster).to receive(:start).and_return(sidekiq_worker_pids)
+ allow(Gitlab::SidekiqCluster).to receive(:start).and_return(waiter_threads)
+ allow(process_status).to receive(:success?).and_return(true)
+ allow(cli).to receive(:exit)
+
+ waiter_threads.each.with_index do |thread, i|
+ allow(thread).to receive(:join)
+ allow(thread).to receive(:pid).and_return(sidekiq_worker_pids[i])
+ allow(thread).to receive(:value).and_return(process_status)
+ end
+ end
+
+ context 'when one of the workers has been terminated gracefully' do
+ it 'stops the entire process cluster' do
+ expect(MetricsServer).to receive(:start_for_sidekiq).once.and_return(metrics_server_pid)
+ expect(supervisor).to receive(:supervise).and_yield([2, 99])
+ expect(supervisor).to receive(:shutdown)
+ expect(cli).not_to receive(:exit).with(1)
+
+ cli.run(%w(foo))
+ end
+ end
+
+ context 'when one of the workers has failed' do
+ it 'stops the entire process cluster and exits with a non-zero code' do
+ expect(MetricsServer).to receive(:start_for_sidekiq).once.and_return(metrics_server_pid)
+ expect(supervisor).to receive(:supervise).and_yield([2, 99])
+ expect(supervisor).to receive(:shutdown)
+ expect(process_status).to receive(:success?).and_return(false)
+ expect(cli).to receive(:exit).with(1)
+
+ cli.run(%w(foo))
+ end
end
it 'stops the entire process cluster if one of the workers has been terminated' do
diff --git a/spec/config/application_spec.rb b/spec/config/application_spec.rb
index 94fecc26e7f..7b64ad4a9b9 100644
--- a/spec/config/application_spec.rb
+++ b/spec/config/application_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Application do # rubocop:disable RSpec/FilePath
+RSpec.describe Gitlab::Application, feature_category: :scalability do # rubocop:disable RSpec/FilePath
using RSpec::Parameterized::TableSyntax
filtered_param = ActiveSupport::ParameterFilter::FILTERED
diff --git a/spec/config/inject_enterprise_edition_module_spec.rb b/spec/config/inject_enterprise_edition_module_spec.rb
index 96fc26fc80a..47cb36c569e 100644
--- a/spec/config/inject_enterprise_edition_module_spec.rb
+++ b/spec/config/inject_enterprise_edition_module_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe InjectEnterpriseEditionModule do
+RSpec.describe InjectEnterpriseEditionModule, feature_category: :fulfillment_developer_productivity do
let(:extension_name) { 'FF' }
let(:extension_namespace) { Module.new }
let(:fish_name) { 'Fish' }
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index ec306837361..cf2146bdf77 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'mail_room.yml' do
+RSpec.describe 'mail_room.yml', feature_category: :service_desk do
include StubENV
let(:mailroom_config_path) { 'config/mail_room.yml' }
diff --git a/spec/config/object_store_settings_spec.rb b/spec/config/object_store_settings_spec.rb
index 9275c809550..7b4fa495288 100644
--- a/spec/config/object_store_settings_spec.rb
+++ b/spec/config/object_store_settings_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require Rails.root.join('config', 'object_store_settings.rb')
-RSpec.describe ObjectStoreSettings do
+RSpec.describe ObjectStoreSettings, feature_category: :not_owned do
describe '#parse!' do
let(:settings) { Settingslogic.new(config) }
@@ -70,7 +70,6 @@ RSpec.describe ObjectStoreSettings do
expect(settings.artifacts['object_store']['enabled']).to be true
expect(settings.artifacts['object_store']['connection']).to eq(connection)
expect(settings.artifacts['object_store']['direct_upload']).to be true
- expect(settings.artifacts['object_store']['background_upload']).to be false
expect(settings.artifacts['object_store']['proxy_download']).to be false
expect(settings.artifacts['object_store']['remote_directory']).to eq('artifacts')
expect(settings.artifacts['object_store']['bucket_prefix']).to eq(nil)
@@ -81,7 +80,6 @@ RSpec.describe ObjectStoreSettings do
expect(settings.lfs['object_store']['enabled']).to be true
expect(settings.lfs['object_store']['connection']).to eq(connection)
expect(settings.lfs['object_store']['direct_upload']).to be true
- expect(settings.lfs['object_store']['background_upload']).to be false
expect(settings.lfs['object_store']['proxy_download']).to be true
expect(settings.lfs['object_store']['remote_directory']).to eq('lfs-objects')
expect(settings.lfs['object_store']['bucket_prefix']).to eq(nil)
@@ -200,7 +198,6 @@ RSpec.describe ObjectStoreSettings do
'enabled' => true,
'remote_directory' => 'some-bucket',
'direct_upload' => false,
- 'background_upload' => true,
'proxy_download' => false
}
end
@@ -215,9 +212,7 @@ RSpec.describe ObjectStoreSettings do
expect(settings.artifacts['object_store']).to be_nil
expect(settings.lfs['object_store']['remote_directory']).to eq('some-bucket')
expect(settings.lfs['object_store']['bucket_prefix']).to eq(nil)
- # Disable background_upload, regardless of the input config
expect(settings.lfs['object_store']['direct_upload']).to eq(true)
- expect(settings.lfs['object_store']['background_upload']).to eq(false)
expect(settings.external_diffs['object_store']).to be_nil
end
end
@@ -230,7 +225,6 @@ RSpec.describe ObjectStoreSettings do
expect(settings['enabled']).to be false
expect(settings['direct_upload']).to be true
- expect(settings['background_upload']).to be false
expect(settings['remote_directory']).to be nil
expect(settings['bucket_prefix']).to be nil
end
@@ -245,7 +239,6 @@ RSpec.describe ObjectStoreSettings do
expect(settings['enabled']).to be true
expect(settings['direct_upload']).to be true
- expect(settings['background_upload']).to be false
expect(settings['remote_directory']).to eq 'artifacts'
expect(settings['bucket_prefix']).to be nil
end
diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb
index fe2081fa5de..4917b043812 100644
--- a/spec/config/settings_spec.rb
+++ b/spec/config/settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Settings do
+RSpec.describe Settings, feature_category: :authentication_and_authorization do
describe 'omniauth' do
it 'defaults to enabled' do
expect(described_class.omniauth.enabled).to be true
diff --git a/spec/config/smime_signature_settings_spec.rb b/spec/config/smime_signature_settings_spec.rb
index 5ce6fdd975b..477ad4a74ed 100644
--- a/spec/config/smime_signature_settings_spec.rb
+++ b/spec/config/smime_signature_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SmimeSignatureSettings do
+RSpec.describe SmimeSignatureSettings, feature_category: :not_owned do
describe '.parse' do
let(:default_smime_key) { Rails.root.join('.gitlab_smime_key') }
let(:default_smime_cert) { Rails.root.join('.gitlab_smime_cert') }
diff --git a/spec/contracts/consumer/.node-version b/spec/contracts/consumer/.node-version
deleted file mode 100644
index 18711d290ea..00000000000
--- a/spec/contracts/consumer/.node-version
+++ /dev/null
@@ -1 +0,0 @@
-14.17.5
diff --git a/spec/contracts/consumer/fixtures/project/merge_request/diffs_batch.fixture.js b/spec/contracts/consumer/fixtures/project/merge_requests/diffs_batch.fixture.js
index 673aad721b3..673aad721b3 100644
--- a/spec/contracts/consumer/fixtures/project/merge_request/diffs_batch.fixture.js
+++ b/spec/contracts/consumer/fixtures/project/merge_requests/diffs_batch.fixture.js
diff --git a/spec/contracts/consumer/fixtures/project/merge_request/diffs_metadata.fixture.js b/spec/contracts/consumer/fixtures/project/merge_requests/diffs_metadata.fixture.js
index 2fee4a02023..2fee4a02023 100644
--- a/spec/contracts/consumer/fixtures/project/merge_request/diffs_metadata.fixture.js
+++ b/spec/contracts/consumer/fixtures/project/merge_requests/diffs_metadata.fixture.js
diff --git a/spec/contracts/consumer/fixtures/project/merge_request/discussions.fixture.js b/spec/contracts/consumer/fixtures/project/merge_requests/discussions.fixture.js
index 8c392395e1c..8c392395e1c 100644
--- a/spec/contracts/consumer/fixtures/project/merge_request/discussions.fixture.js
+++ b/spec/contracts/consumer/fixtures/project/merge_requests/discussions.fixture.js
diff --git a/spec/contracts/consumer/fixtures/project/pipeline_schedule/update_pipeline_schedule.fixture.js b/spec/contracts/consumer/fixtures/project/pipeline_schedules/update_pipeline_schedule.fixture.js
index acfab14851a..acfab14851a 100644
--- a/spec/contracts/consumer/fixtures/project/pipeline_schedule/update_pipeline_schedule.fixture.js
+++ b/spec/contracts/consumer/fixtures/project/pipeline_schedules/update_pipeline_schedule.fixture.js
diff --git a/spec/contracts/consumer/fixtures/project/pipeline/create_a_new_pipeline.fixture.js b/spec/contracts/consumer/fixtures/project/pipelines/create_a_new_pipeline.fixture.js
index 68063d2fb0c..68063d2fb0c 100644
--- a/spec/contracts/consumer/fixtures/project/pipeline/create_a_new_pipeline.fixture.js
+++ b/spec/contracts/consumer/fixtures/project/pipelines/create_a_new_pipeline.fixture.js
diff --git a/spec/contracts/consumer/fixtures/project/pipeline/delete_pipeline.fixture.js b/spec/contracts/consumer/fixtures/project/pipelines/delete_pipeline.fixture.js
index 2e3e7355b99..2e3e7355b99 100644
--- a/spec/contracts/consumer/fixtures/project/pipeline/delete_pipeline.fixture.js
+++ b/spec/contracts/consumer/fixtures/project/pipelines/delete_pipeline.fixture.js
diff --git a/spec/contracts/consumer/fixtures/project/pipeline/get_list_project_pipelines.fixture.js b/spec/contracts/consumer/fixtures/project/pipelines/get_list_project_pipelines.fixture.js
index a982e927572..a982e927572 100644
--- a/spec/contracts/consumer/fixtures/project/pipeline/get_list_project_pipelines.fixture.js
+++ b/spec/contracts/consumer/fixtures/project/pipelines/get_list_project_pipelines.fixture.js
diff --git a/spec/contracts/consumer/fixtures/project/pipeline/get_pipeline_header_data.fixture.js b/spec/contracts/consumer/fixtures/project/pipelines/get_pipeline_header_data.fixture.js
index b14a230d2e0..b14a230d2e0 100644
--- a/spec/contracts/consumer/fixtures/project/pipeline/get_pipeline_header_data.fixture.js
+++ b/spec/contracts/consumer/fixtures/project/pipelines/get_pipeline_header_data.fixture.js
diff --git a/spec/contracts/consumer/package.json b/spec/contracts/consumer/package.json
index 6d3feaa6d4c..60f268806de 100644
--- a/spec/contracts/consumer/package.json
+++ b/spec/contracts/consumer/package.json
@@ -22,5 +22,8 @@
"devDependencies": {
"@babel/preset-env": "^7.18.2",
"babel-jest": "^28.1.1"
+ },
+ "config": {
+ "pact_do_not_track": true
}
}
diff --git a/spec/contracts/consumer/specs/project/merge_request/show.spec.js b/spec/contracts/consumer/specs/project/merge_requests/show.spec.js
index 4183e19435a..fcc0e117e2d 100644
--- a/spec/contracts/consumer/specs/project/merge_request/show.spec.js
+++ b/spec/contracts/consumer/specs/project/merge_requests/show.spec.js
@@ -1,32 +1,32 @@
import { pactWith } from 'jest-pact';
-import { DiffsBatch } from '../../../fixtures/project/merge_request/diffs_batch.fixture';
-import { Discussions } from '../../../fixtures/project/merge_request/discussions.fixture';
-import { DiffsMetadata } from '../../../fixtures/project/merge_request/diffs_metadata.fixture';
+import { DiffsBatch } from '../../../fixtures/project/merge_requests/diffs_batch.fixture';
+import { Discussions } from '../../../fixtures/project/merge_requests/discussions.fixture';
+import { DiffsMetadata } from '../../../fixtures/project/merge_requests/diffs_metadata.fixture';
import {
getDiffsBatch,
getDiffsMetadata,
getDiscussions,
} from '../../../resources/api/project/merge_requests';
-const CONSUMER_NAME = 'MergeRequest#show';
+const CONSUMER_NAME = 'MergeRequests#show';
const CONSUMER_LOG = '../logs/consumer.log';
-const CONTRACT_DIR = '../contracts/project/merge_request/show';
-const DIFFS_BATCH_PROVIDER_NAME = 'Merge Request Diffs Batch Endpoint';
-const DISCUSSIONS_PROVIDER_NAME = 'Merge Request Discussions Endpoint';
-const DIFFS_METADATA_PROVIDER_NAME = 'Merge Request Diffs Metadata Endpoint';
+const CONTRACT_DIR = '../contracts/project/merge_requests/show';
+const GET_DIFFS_BATCH_PROVIDER_NAME = 'GET diffs batch';
+const GET_DISCUSSIONS_PROVIDER_NAME = 'GET discussions';
+const GET_DIFFS_METADATA_PROVIDER_NAME = 'GET diffs metadata';
// API endpoint: /merge_requests/:id/diffs_batch.json
pactWith(
{
consumer: CONSUMER_NAME,
- provider: DIFFS_BATCH_PROVIDER_NAME,
+ provider: GET_DIFFS_BATCH_PROVIDER_NAME,
log: CONSUMER_LOG,
dir: CONTRACT_DIR,
},
(provider) => {
- describe(DIFFS_BATCH_PROVIDER_NAME, () => {
+ describe(GET_DIFFS_BATCH_PROVIDER_NAME, () => {
beforeEach(() => {
const interaction = {
...DiffsBatch.scenario,
@@ -50,13 +50,13 @@ pactWith(
pactWith(
{
consumer: CONSUMER_NAME,
- provider: DISCUSSIONS_PROVIDER_NAME,
+ provider: GET_DISCUSSIONS_PROVIDER_NAME,
log: CONSUMER_LOG,
dir: CONTRACT_DIR,
},
(provider) => {
- describe(DISCUSSIONS_PROVIDER_NAME, () => {
+ describe(GET_DISCUSSIONS_PROVIDER_NAME, () => {
beforeEach(() => {
const interaction = {
...Discussions.scenario,
@@ -80,13 +80,13 @@ pactWith(
pactWith(
{
consumer: CONSUMER_NAME,
- provider: DIFFS_METADATA_PROVIDER_NAME,
+ provider: GET_DIFFS_METADATA_PROVIDER_NAME,
log: CONSUMER_LOG,
dir: CONTRACT_DIR,
},
(provider) => {
- describe(DIFFS_METADATA_PROVIDER_NAME, () => {
+ describe(GET_DIFFS_METADATA_PROVIDER_NAME, () => {
beforeEach(() => {
const interaction = {
...DiffsMetadata.scenario,
diff --git a/spec/contracts/consumer/specs/project/pipeline_schedule/edit.spec.js b/spec/contracts/consumer/specs/project/pipeline_schedules/edit.spec.js
index 117e6754255..0924b1b3b3d 100644
--- a/spec/contracts/consumer/specs/project/pipeline_schedule/edit.spec.js
+++ b/spec/contracts/consumer/specs/project/pipeline_schedules/edit.spec.js
@@ -1,12 +1,12 @@
import { pactWith } from 'jest-pact';
-import { UpdatePipelineSchedule } from '../../../fixtures/project/pipeline_schedule/update_pipeline_schedule.fixture';
+import { UpdatePipelineSchedule } from '../../../fixtures/project/pipeline_schedules/update_pipeline_schedule.fixture';
import { updatePipelineSchedule } from '../../../resources/api/pipeline_schedules';
const CONSUMER_NAME = 'PipelineSchedules#edit';
const CONSUMER_LOG = '../logs/consumer.log';
-const CONTRACT_DIR = '../contracts/project/pipeline_schedule/edit';
-const PROVIDER_NAME = 'PUT Edit a pipeline schedule';
+const CONTRACT_DIR = '../contracts/project/pipeline_schedules/edit';
+const PROVIDER_NAME = 'PUT edit a pipeline schedule';
// API endpoint: /pipelines.json
pactWith(
diff --git a/spec/contracts/consumer/specs/project/pipeline/index.spec.js b/spec/contracts/consumer/specs/project/pipelines/index.spec.js
index 1453435d637..14bad31a763 100644
--- a/spec/contracts/consumer/specs/project/pipeline/index.spec.js
+++ b/spec/contracts/consumer/specs/project/pipelines/index.spec.js
@@ -1,12 +1,12 @@
import { pactWith } from 'jest-pact';
-import { ProjectPipelines } from '../../../fixtures/project/pipeline/get_list_project_pipelines.fixture';
+import { ProjectPipelines } from '../../../fixtures/project/pipelines/get_list_project_pipelines.fixture';
import { getProjectPipelines } from '../../../resources/api/project/pipelines';
const CONSUMER_NAME = 'Pipelines#index';
const CONSUMER_LOG = '../logs/consumer.log';
-const CONTRACT_DIR = '../contracts/project/pipeline/index';
-const PROVIDER_NAME = 'GET List project pipelines';
+const CONTRACT_DIR = '../contracts/project/pipelines/index';
+const PROVIDER_NAME = 'GET list project pipelines';
// API endpoint: /pipelines.json
pactWith(
diff --git a/spec/contracts/consumer/specs/project/pipeline/new.spec.js b/spec/contracts/consumer/specs/project/pipelines/new.spec.js
index c3824d5979e..9e381a61670 100644
--- a/spec/contracts/consumer/specs/project/pipeline/new.spec.js
+++ b/spec/contracts/consumer/specs/project/pipelines/new.spec.js
@@ -1,12 +1,12 @@
import { pactWith } from 'jest-pact';
-import { NewProjectPipeline } from '../../../fixtures/project/pipeline/create_a_new_pipeline.fixture';
+import { NewProjectPipeline } from '../../../fixtures/project/pipelines/create_a_new_pipeline.fixture';
import { postProjectPipelines } from '../../../resources/api/project/pipelines';
const CONSUMER_NAME = 'Pipelines#new';
const CONSUMER_LOG = '../logs/consumer.log';
-const CONTRACT_DIR = '../contracts/project/pipeline/new';
-const PROVIDER_NAME = 'POST Create a new pipeline';
+const CONTRACT_DIR = '../contracts/project/pipelines/new';
+const PROVIDER_NAME = 'POST create a new pipeline';
// API endpoint: /pipelines.json
pactWith(
diff --git a/spec/contracts/consumer/specs/project/pipeline/show.spec.js b/spec/contracts/consumer/specs/project/pipelines/show.spec.js
index be6abb78eb5..97ad9dbbc9d 100644
--- a/spec/contracts/consumer/specs/project/pipeline/show.spec.js
+++ b/spec/contracts/consumer/specs/project/pipelines/show.spec.js
@@ -3,14 +3,14 @@ import { GraphQLInteraction } from '@pact-foundation/pact';
import { extractGraphQLQuery } from '../../../helpers/graphql_query_extractor';
-import { PipelineHeaderData } from '../../../fixtures/project/pipeline/get_pipeline_header_data.fixture';
-import { DeletePipeline } from '../../../fixtures/project/pipeline/delete_pipeline.fixture';
+import { PipelineHeaderData } from '../../../fixtures/project/pipelines/get_pipeline_header_data.fixture';
+import { DeletePipeline } from '../../../fixtures/project/pipelines/delete_pipeline.fixture';
import { getPipelineHeaderDataRequest, deletePipeline } from '../../../resources/graphql/pipelines';
const CONSUMER_NAME = 'Pipelines#show';
const CONSUMER_LOG = '../logs/consumer.log';
-const CONTRACT_DIR = '../contracts/project/pipeline/show';
+const CONTRACT_DIR = '../contracts/project/pipelines/show';
const GET_PIPELINE_HEADER_DATA_PROVIDER_NAME = 'GET pipeline header data';
const DELETE_PIPELINE_PROVIDER_NAME = 'DELETE pipeline';
diff --git a/spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_batch_endpoint.json b/spec/contracts/contracts/project/merge_requests/show/mergerequests#show-get_diffs_batch.json
index 3fa13766766..809a3aeec13 100644
--- a/spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_batch_endpoint.json
+++ b/spec/contracts/contracts/project/merge_requests/show/mergerequests#show-get_diffs_batch.json
@@ -1,9 +1,9 @@
{
"consumer": {
- "name": "MergeRequest#show"
+ "name": "MergeRequests#show"
},
"provider": {
- "name": "Merge Request Diffs Batch Endpoint"
+ "name": "GET diffs batch"
},
"interactions": [
{
diff --git a/spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_metadata_endpoint.json b/spec/contracts/contracts/project/merge_requests/show/mergerequests#show-get_diffs_metadata.json
index c59a3d55f43..3a7b0707445 100644
--- a/spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_metadata_endpoint.json
+++ b/spec/contracts/contracts/project/merge_requests/show/mergerequests#show-get_diffs_metadata.json
@@ -1,9 +1,9 @@
{
"consumer": {
- "name": "MergeRequest#show"
+ "name": "MergeRequests#show"
},
"provider": {
- "name": "Merge Request Diffs Metadata Endpoint"
+ "name": "GET diffs metadata"
},
"interactions": [
{
@@ -220,4 +220,4 @@
"version": "2.0.0"
}
}
-}
+} \ No newline at end of file
diff --git a/spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_discussions_endpoint.json b/spec/contracts/contracts/project/merge_requests/show/mergerequests#show-get_discussions.json
index ecaf9c123af..74f75e7eb7b 100644
--- a/spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_discussions_endpoint.json
+++ b/spec/contracts/contracts/project/merge_requests/show/mergerequests#show-get_discussions.json
@@ -1,9 +1,9 @@
{
"consumer": {
- "name": "MergeRequest#show"
+ "name": "MergeRequests#show"
},
"provider": {
- "name": "Merge Request Discussions Endpoint"
+ "name": "GET discussions"
},
"interactions": [
{
diff --git a/spec/contracts/contracts/project/pipeline_schedule/edit/pipelineschedules#edit-put_edit_a_pipeline_schedule.json b/spec/contracts/contracts/project/pipeline_schedules/edit/pipelineschedules#edit-put_edit_a_pipeline_schedule.json
index e0dd68dc230..59feebcfc64 100644
--- a/spec/contracts/contracts/project/pipeline_schedule/edit/pipelineschedules#edit-put_edit_a_pipeline_schedule.json
+++ b/spec/contracts/contracts/project/pipeline_schedules/edit/pipelineschedules#edit-put_edit_a_pipeline_schedule.json
@@ -3,7 +3,7 @@
"name": "PipelineSchedules#edit"
},
"provider": {
- "name": "PUT Edit a pipeline schedule"
+ "name": "PUT edit a pipeline schedule"
},
"interactions": [
{
diff --git a/spec/contracts/contracts/project/pipeline/index/pipelines#index-get_list_project_pipelines.json b/spec/contracts/contracts/project/pipelines/index/pipelines#index-get_list_project_pipelines.json
index 2ebfd1bfdf2..01c6563f76a 100644
--- a/spec/contracts/contracts/project/pipeline/index/pipelines#index-get_list_project_pipelines.json
+++ b/spec/contracts/contracts/project/pipelines/index/pipelines#index-get_list_project_pipelines.json
@@ -3,7 +3,7 @@
"name": "Pipelines#index"
},
"provider": {
- "name": "GET List project pipelines"
+ "name": "GET list project pipelines"
},
"interactions": [
{
@@ -126,8 +126,7 @@
"committer_name": "John Doe",
"committer_email": "jdoe@gitlab.com",
"committed_date": "2022-06-10T22:02:10.000+00:00",
- "trailers": {
- },
+ "trailers": {},
"web_url": "https://gitlab.com/gitlab-org/gitlab/-/commit/f559253c514d9ab707c66e",
"author": null,
"author_gravatar_url": "https://secure.gravatar.com/avatar/d85e45af29611ac2c1395e3c3d6ec5d6?s=80&d=identicon",
@@ -141,9 +140,7 @@
"full_name": "GitLab.org / GitLab"
},
"triggered_by": null,
- "triggered": [
-
- ]
+ "triggered": []
}
],
"count": {
@@ -198,9 +195,6 @@
"match": "regex",
"regex": "^(push|web|trigger|schedule|api|external|pipeline|chat|webide|merge_request_event|external_pull_request_event|parent_pipeline|ondemand_dast_scan|ondemand_dast_validation)$"
},
- "$.body.pipelines[*].name": {
- "match": "type"
- },
"$.body.pipelines[*].created_at": {
"match": "regex",
"regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
@@ -338,9 +332,6 @@
"$.body.pipelines[*].details.name": {
"match": "type"
},
- "$.body.pipelines[*].details.event_type_name": {
- "match": "type"
- },
"$.body.pipelines[*].details.manual_actions": {
"min": 1
},
diff --git a/spec/contracts/contracts/project/pipeline/new/pipelines#new-post_create_a_new_pipeline.json b/spec/contracts/contracts/project/pipelines/new/pipelines#new-post_create_a_new_pipeline.json
index 4627f0cb0bf..4be6b1e3cab 100644
--- a/spec/contracts/contracts/project/pipeline/new/pipelines#new-post_create_a_new_pipeline.json
+++ b/spec/contracts/contracts/project/pipelines/new/pipelines#new-post_create_a_new_pipeline.json
@@ -3,7 +3,7 @@
"name": "Pipelines#new"
},
"provider": {
- "name": "POST Create a new pipeline"
+ "name": "POST create a new pipeline"
},
"interactions": [
{
@@ -29,7 +29,7 @@
"matchingRules": {
"$.body": {
"match": "regex",
- "regex": "You are being <a href=\\\"(.)+\\/pipelines\\/[0-9]+\\\">redirected<\\/a>."
+ "regex": "You are being <a href=\\\"(.)+\\\">redirected<\\/a>."
}
}
}
diff --git a/spec/contracts/contracts/project/pipeline/show/pipelines#show-delete_pipeline.json b/spec/contracts/contracts/project/pipelines/show/pipelines#show-delete_pipeline.json
index 795c8a6e197..795c8a6e197 100644
--- a/spec/contracts/contracts/project/pipeline/show/pipelines#show-delete_pipeline.json
+++ b/spec/contracts/contracts/project/pipelines/show/pipelines#show-delete_pipeline.json
diff --git a/spec/contracts/contracts/project/pipeline/show/pipelines#show-get_pipeline_header_data.json b/spec/contracts/contracts/project/pipelines/show/pipelines#show-get_pipeline_header_data.json
index 2d775dc0f61..2d775dc0f61 100644
--- a/spec/contracts/contracts/project/pipeline/show/pipelines#show-get_pipeline_header_data.json
+++ b/spec/contracts/contracts/project/pipelines/show/pipelines#show-get_pipeline_header_data.json
diff --git a/spec/contracts/provider/helpers/contract_source_helper.rb b/spec/contracts/provider/helpers/contract_source_helper.rb
new file mode 100644
index 00000000000..5fc2e3ffc0d
--- /dev/null
+++ b/spec/contracts/provider/helpers/contract_source_helper.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Provider
+ module ContractSourceHelper
+ QA_PACT_BROKER_HOST = "http://localhost:9292/pacts"
+ PREFIX_PATHS = {
+ rake: "../../../contracts/contracts/project",
+ spec: "../contracts/project"
+ }.freeze
+ SUB_PATH_REGEX = %r{project/(?<file_path>.*?)_helper.rb}.freeze
+
+ class << self
+ def contract_location(requester, file_path)
+ raise ArgumentError, 'requester must be :rake or :spec' unless [:rake, :spec].include? requester
+
+ relevant_path = file_path.match(SUB_PATH_REGEX)[:file_path].split('/')
+
+ ENV["PACT_BROKER"] ? pact_broker_url(relevant_path) : local_contract_location(requester, relevant_path)
+ end
+
+ def pact_broker_url(file_path)
+ provider_url = "provider/#{construct_provider_url_path(file_path)}"
+ consumer_url = "consumer/#{construct_consumer_url_path(file_path)}"
+
+ "#{QA_PACT_BROKER_HOST}/#{provider_url}/#{consumer_url}/latest"
+ end
+
+ def construct_provider_url_path(file_path)
+ split_name = file_path[2].split('_')
+
+ split_name[0] = split_name[0].upcase
+ split_name.join("%20")
+ end
+
+ def construct_consumer_url_path(file_path)
+ "#{file_path[0].split('_').map(&:capitalize).join}%23#{file_path[1]}"
+ end
+
+ def local_contract_location(requester, file_path)
+ contract_path = construct_local_contract_path(file_path)
+ prefix_path = requester == :rake ? File.expand_path(PREFIX_PATHS[requester], __dir__) : PREFIX_PATHS[requester]
+
+ "#{prefix_path}#{contract_path}"
+ end
+
+ def construct_local_contract_path(file_path)
+ contract_file_name = "#{file_path[0].tr('_', '')}##{file_path[1]}-#{file_path[2]}.json"
+
+ "/#{file_path[0]}/#{file_path[1]}/#{contract_file_name}"
+ end
+ end
+ end
+end
diff --git a/spec/contracts/provider/helpers/publish_contract_helper.rb b/spec/contracts/provider/helpers/publish_contract_helper.rb
index 102a73d87ee..ddf1e1fd0ed 100644
--- a/spec/contracts/provider/helpers/publish_contract_helper.rb
+++ b/spec/contracts/provider/helpers/publish_contract_helper.rb
@@ -2,15 +2,15 @@
module Provider
module PublishContractHelper
- PROVIDER_VERSION = ENV['GIT_COMMIT'] || `git rev-parse --verify HEAD`.strip
- PROVIDER_BRANCH = ENV['GIT_BRANCH'] || `git name-rev --name-only HEAD`.strip
+ PROVIDER_VERSION = ENV["GIT_COMMIT"] || `git rev-parse --verify HEAD`.strip
+ PROVIDER_BRANCH = ENV["GIT_BRANCH"] || `git name-rev --name-only HEAD`.strip
PUBLISH_FLAG = true
def self.publish_contract_setup
- @setup ||= -> {
- app_version PROVIDER_VERSION
- app_version_branch PROVIDER_BRANCH
- publish_verification_results PUBLISH_FLAG
+ ->(app_version, app_version_branch, publish_verification_results) {
+ app_version.call(PROVIDER_VERSION)
+ app_version_branch.call(PROVIDER_BRANCH)
+ publish_verification_results.call(PUBLISH_FLAG)
}
end
end
diff --git a/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_batch_helper.rb b/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_batch_helper.rb
deleted file mode 100644
index 71f302f2c44..00000000000
--- a/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_batch_helper.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../../../spec_helper'
-require_relative '../../../../states/project/merge_request/show_state'
-
-module Provider
- module DiffsBatchHelper
- Pact.service_provider "Merge Request Diffs Batch Endpoint" do
- app { Environments::Test.app }
-
- honours_pact_with 'MergeRequest#show' do
- pact_uri '../contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_batch_endpoint.json'
- end
-
- Provider::PublishContractHelper.publish_contract_setup.call
- end
- end
-end
diff --git a/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_metadata_helper.rb b/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_metadata_helper.rb
deleted file mode 100644
index 60a3abea5ae..00000000000
--- a/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_metadata_helper.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../../../spec_helper'
-require_relative '../../../../states/project/merge_request/show_state'
-
-module Provider
- module DiffsMetadataHelper
- Pact.service_provider "Merge Request Diffs Metadata Endpoint" do
- app { Environments::Test.app }
-
- honours_pact_with 'MergeRequest#show' do
- pact_uri '../contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_metadata_endpoint.json'
- end
-
- app_version Provider::PublishContractHelper::PROVIDER_VERSION
- app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
- publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
- end
- end
-end
diff --git a/spec/contracts/provider/pact_helpers/project/merge_request/show/discussions_helper.rb b/spec/contracts/provider/pact_helpers/project/merge_request/show/discussions_helper.rb
deleted file mode 100644
index b9308af0a1a..00000000000
--- a/spec/contracts/provider/pact_helpers/project/merge_request/show/discussions_helper.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../../../spec_helper'
-require_relative '../../../../states/project/merge_request/show_state'
-
-module Provider
- module DiscussionsHelper
- Pact.service_provider "Merge Request Discussions Endpoint" do
- app { Environments::Test.app }
-
- honours_pact_with 'MergeRequest#show' do
- pact_uri '../contracts/project/merge_request/show/mergerequest#show-merge_request_discussions_endpoint.json'
- end
-
- app_version Provider::PublishContractHelper::PROVIDER_VERSION
- app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
- publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
- end
- end
-end
diff --git a/spec/contracts/provider/pact_helpers/project/merge_requests/show/get_diffs_batch_helper.rb b/spec/contracts/provider/pact_helpers/project/merge_requests/show/get_diffs_batch_helper.rb
new file mode 100644
index 00000000000..aa97a07c07b
--- /dev/null
+++ b/spec/contracts/provider/pact_helpers/project/merge_requests/show/get_diffs_batch_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require_relative "../../../../spec_helper"
+require_relative "../../../../helpers/contract_source_helper"
+require_relative "../../../../helpers/publish_contract_helper"
+require_relative "../../../../states/project/merge_requests/show_state"
+
+module Provider
+ module DiffsBatchHelper
+ Pact.service_provider "GET diffs batch" do
+ app { Environments::Test.app }
+
+ honours_pact_with "MergeRequests#show" do
+ pact_uri Provider::ContractSourceHelper.contract_location(:spec, __FILE__)
+ end
+
+ Provider::PublishContractHelper.publish_contract_setup.call(
+ method(:app_version),
+ method(:app_version_branch),
+ method(:publish_verification_results)
+ )
+ end
+ end
+end
diff --git a/spec/contracts/provider/pact_helpers/project/merge_requests/show/get_diffs_metadata_helper.rb b/spec/contracts/provider/pact_helpers/project/merge_requests/show/get_diffs_metadata_helper.rb
new file mode 100644
index 00000000000..891585b0066
--- /dev/null
+++ b/spec/contracts/provider/pact_helpers/project/merge_requests/show/get_diffs_metadata_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require_relative "../../../../spec_helper"
+require_relative "../../../../helpers/contract_source_helper"
+require_relative "../../../../helpers/publish_contract_helper"
+require_relative "../../../../states/project/merge_requests/show_state"
+
+module Provider
+ module DiffsMetadataHelper
+ Pact.service_provider "GET diffs metadata" do
+ app { Environments::Test.app }
+
+ honours_pact_with "MergeRequests#show" do
+ pact_uri Provider::ContractSourceHelper.contract_location(:spec, __FILE__)
+ end
+
+ Provider::PublishContractHelper.publish_contract_setup.call(
+ method(:app_version),
+ method(:app_version_branch),
+ method(:publish_verification_results)
+ )
+ end
+ end
+end
diff --git a/spec/contracts/provider/pact_helpers/project/merge_requests/show/get_discussions_helper.rb b/spec/contracts/provider/pact_helpers/project/merge_requests/show/get_discussions_helper.rb
new file mode 100644
index 00000000000..229818366ca
--- /dev/null
+++ b/spec/contracts/provider/pact_helpers/project/merge_requests/show/get_discussions_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require_relative "../../../../spec_helper"
+require_relative "../../../../helpers/contract_source_helper"
+require_relative "../../../../helpers/publish_contract_helper"
+require_relative "../../../../states/project/merge_requests/show_state"
+
+module Provider
+ module DiscussionsHelper
+ Pact.service_provider "GET discussions" do
+ app { Environments::Test.app }
+
+ honours_pact_with "MergeRequests#show" do
+ pact_uri Provider::ContractSourceHelper.contract_location(:spec, __FILE__)
+ end
+
+ Provider::PublishContractHelper.publish_contract_setup.call(
+ method(:app_version),
+ method(:app_version_branch),
+ method(:publish_verification_results)
+ )
+ end
+ end
+end
diff --git a/spec/contracts/provider/pact_helpers/project/pipeline/index/create_a_new_pipeline_helper.rb b/spec/contracts/provider/pact_helpers/project/pipeline/index/create_a_new_pipeline_helper.rb
deleted file mode 100644
index 2af960bc9fd..00000000000
--- a/spec/contracts/provider/pact_helpers/project/pipeline/index/create_a_new_pipeline_helper.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../../../spec_helper'
-require_relative '../../../../states/project/pipeline/new_state'
-
-module Provider
- module CreateNewPipelineHelper
- Pact.service_provider "POST Create a new pipeline" do
- app { Environments::Test.app }
-
- honours_pact_with 'Pipelines#new' do
- pact_uri '../contracts/project/pipeline/new/pipelines#new-post_create_a_new_pipeline.json'
- end
-
- app_version Provider::PublishContractHelper::PROVIDER_VERSION
- app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
- publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
- end
- end
-end
diff --git a/spec/contracts/provider/pact_helpers/project/pipeline/index/get_list_project_pipelines_helper.rb b/spec/contracts/provider/pact_helpers/project/pipeline/index/get_list_project_pipelines_helper.rb
deleted file mode 100644
index 37cddd1b80e..00000000000
--- a/spec/contracts/provider/pact_helpers/project/pipeline/index/get_list_project_pipelines_helper.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../../../spec_helper'
-require_relative '../../../../states/project/pipeline/index_state'
-
-module Provider
- module GetListProjectPipelinesHelper
- Pact.service_provider "GET List project pipelines" do
- app { Environments::Test.app }
-
- honours_pact_with 'Pipelines#index' do
- pact_uri '../contracts/project/project/pipeline/index/pipelines#index-get_list_project_pipelines.json'
- end
-
- app_version Provider::PublishContractHelper::PROVIDER_VERSION
- app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
- publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
- end
- end
-end
diff --git a/spec/contracts/provider/pact_helpers/project/pipeline/show/delete_pipeline_helper.rb b/spec/contracts/provider/pact_helpers/project/pipeline/show/delete_pipeline_helper.rb
deleted file mode 100644
index 0455281fcd7..00000000000
--- a/spec/contracts/provider/pact_helpers/project/pipeline/show/delete_pipeline_helper.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../../../spec_helper'
-require_relative '../../../../helpers/publish_contract_helper'
-require_relative '../../../../states/project/pipeline/show_state'
-
-module Provider
- module DeletePipelineHelper
- Pact.service_provider "DELETE pipeline" do
- app { Environments::Test.app }
-
- honours_pact_with 'Pipelines#show' do
- pact_uri '../contracts/project/pipeline/show/pipelines#show-delete_pipeline.json'
- end
-
- app_version Provider::PublishContractHelper::PROVIDER_VERSION
- app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
- publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
- end
- end
-end
diff --git a/spec/contracts/provider/pact_helpers/project/pipeline/show/get_pipeline_header_data_helper.rb b/spec/contracts/provider/pact_helpers/project/pipeline/show/get_pipeline_header_data_helper.rb
deleted file mode 100644
index bce1c4ab3f4..00000000000
--- a/spec/contracts/provider/pact_helpers/project/pipeline/show/get_pipeline_header_data_helper.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../../../spec_helper'
-require_relative '../../../../helpers/publish_contract_helper'
-require_relative '../../../../states/project/pipeline/show_state'
-
-module Provider
- module GetPipelinesHeaderDataHelper
- Pact.service_provider "GET pipeline header data" do
- app { Environments::Test.app }
-
- honours_pact_with 'Pipelines#show' do
- pact_uri '../contracts/project/pipeline/show/pipelines#show-get_project_pipeline_header_data.json'
- # pact_uri 'http://localhost:9292/pacts/provider/GET%20pipeline%20header%20data/consumer/Pipelines%23show/latest'
- end
-
- app_version Provider::PublishContractHelper::PROVIDER_VERSION
- app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
- publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
- end
- end
-end
diff --git a/spec/contracts/provider/pact_helpers/project/pipeline_schedule/update_pipeline_schedule_helper.rb b/spec/contracts/provider/pact_helpers/project/pipeline_schedule/update_pipeline_schedule_helper.rb
deleted file mode 100644
index d95a09abd8d..00000000000
--- a/spec/contracts/provider/pact_helpers/project/pipeline_schedule/update_pipeline_schedule_helper.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../../spec_helper'
-require_relative '../../../states/project/pipeline_schedule/edit_state'
-
-module Provider
- module CreateNewPipelineHelper
- Pact.service_provider "PUT Edit a pipeline schedule" do
- app { Environments::Test.app }
-
- honours_pact_with 'PipelineSchedule#edit' do
- pact_uri '../contracts/project/pipeline_schedule/edit/pipelineschedules#edit-put_edit_a_pipeline_schedule.json'
- end
-
- app_version Provider::PublishContractHelper::PROVIDER_VERSION
- app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
- publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
- end
- end
-end
diff --git a/spec/contracts/provider/pact_helpers/project/pipeline_schedules/edit/put_edit_a_pipeline_schedule_helper.rb b/spec/contracts/provider/pact_helpers/project/pipeline_schedules/edit/put_edit_a_pipeline_schedule_helper.rb
new file mode 100644
index 00000000000..62702fd5f92
--- /dev/null
+++ b/spec/contracts/provider/pact_helpers/project/pipeline_schedules/edit/put_edit_a_pipeline_schedule_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require_relative "../../../../spec_helper"
+require_relative "../../../../helpers/contract_source_helper"
+require_relative "../../../../helpers/publish_contract_helper"
+require_relative "../../../../states/project/pipeline_schedules/edit_state"
+
+module Provider
+ module CreateNewPipelineHelper
+ Pact.service_provider "PUT edit a pipeline schedule" do
+ app { Environments::Test.app }
+
+ honours_pact_with "PipelineSchedules#edit" do
+ pact_uri Provider::ContractSourceHelper.contract_location(:spec, __FILE__)
+ end
+
+ Provider::PublishContractHelper.publish_contract_setup.call(
+ method(:app_version),
+ method(:app_version_branch),
+ method(:publish_verification_results)
+ )
+ end
+ end
+end
diff --git a/spec/contracts/provider/pact_helpers/project/pipelines/index/get_list_project_pipelines_helper.rb b/spec/contracts/provider/pact_helpers/project/pipelines/index/get_list_project_pipelines_helper.rb
new file mode 100644
index 00000000000..03708db2eb2
--- /dev/null
+++ b/spec/contracts/provider/pact_helpers/project/pipelines/index/get_list_project_pipelines_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require_relative "../../../../spec_helper"
+require_relative "../../../../helpers/contract_source_helper"
+require_relative "../../../../helpers/publish_contract_helper"
+require_relative "../../../../states/project/pipelines/index_state"
+
+module Provider
+ module GetListProjectPipelinesHelper
+ Pact.service_provider "GET list project pipelines" do
+ app { Environments::Test.app }
+
+ honours_pact_with "Pipelines#index" do
+ pact_uri Provider::ContractSourceHelper.contract_location(:spec, __FILE__)
+ end
+
+ Provider::PublishContractHelper.publish_contract_setup.call(
+ method(:app_version),
+ method(:app_version_branch),
+ method(:publish_verification_results)
+ )
+ end
+ end
+end
diff --git a/spec/contracts/provider/pact_helpers/project/pipelines/new/post_create_a_new_pipeline_helper.rb b/spec/contracts/provider/pact_helpers/project/pipelines/new/post_create_a_new_pipeline_helper.rb
new file mode 100644
index 00000000000..53e5ab61a20
--- /dev/null
+++ b/spec/contracts/provider/pact_helpers/project/pipelines/new/post_create_a_new_pipeline_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require_relative "../../../../spec_helper"
+require_relative "../../../../helpers/contract_source_helper"
+require_relative "../../../../helpers/publish_contract_helper"
+require_relative "../../../../states/project/pipelines/new_state"
+
+module Provider
+ module CreateNewPipelineHelper
+ Pact.service_provider "POST create a new pipeline" do
+ app { Environments::Test.app }
+
+ honours_pact_with "Pipelines#new" do
+ pact_uri Provider::ContractSourceHelper.contract_location(:spec, __FILE__)
+ end
+
+ Provider::PublishContractHelper.publish_contract_setup.call(
+ method(:app_version),
+ method(:app_version_branch),
+ method(:publish_verification_results)
+ )
+ end
+ end
+end
diff --git a/spec/contracts/provider/pact_helpers/project/pipelines/show/delete_pipeline_helper.rb b/spec/contracts/provider/pact_helpers/project/pipelines/show/delete_pipeline_helper.rb
new file mode 100644
index 00000000000..1801e989c99
--- /dev/null
+++ b/spec/contracts/provider/pact_helpers/project/pipelines/show/delete_pipeline_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require_relative "../../../../spec_helper"
+require_relative "../../../../helpers/contract_source_helper"
+require_relative "../../../../helpers/publish_contract_helper"
+require_relative "../../../../states/project/pipelines/show_state"
+
+module Provider
+ module DeletePipelineHelper
+ Pact.service_provider "DELETE pipeline" do
+ app { Environments::Test.app }
+
+ honours_pact_with "Pipelines#show" do
+ pact_uri Provider::ContractSourceHelper.contract_location(:spec, __FILE__)
+ end
+
+ Provider::PublishContractHelper.publish_contract_setup.call(
+ method(:app_version),
+ method(:app_version_branch),
+ method(:publish_verification_results)
+ )
+ end
+ end
+end
diff --git a/spec/contracts/provider/pact_helpers/project/pipelines/show/get_pipeline_header_data_helper.rb b/spec/contracts/provider/pact_helpers/project/pipelines/show/get_pipeline_header_data_helper.rb
new file mode 100644
index 00000000000..1f3ba9dd007
--- /dev/null
+++ b/spec/contracts/provider/pact_helpers/project/pipelines/show/get_pipeline_header_data_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require_relative "../../../../spec_helper"
+require_relative "../../../../helpers/contract_source_helper"
+require_relative "../../../../helpers/publish_contract_helper"
+require_relative "../../../../states/project/pipelines/show_state"
+
+module Provider
+ module GetPipelinesHeaderDataHelper
+ Pact.service_provider "GET pipeline header data" do
+ app { Environments::Test.app }
+
+ honours_pact_with "Pipelines#show" do
+ pact_uri Provider::ContractSourceHelper.contract_location(:spec, __FILE__)
+ end
+
+ Provider::PublishContractHelper.publish_contract_setup.call(
+ method(:app_version),
+ method(:app_version_branch),
+ method(:publish_verification_results)
+ )
+ end
+ end
+end
diff --git a/spec/contracts/provider/spec_helper.rb b/spec/contracts/provider/spec_helper.rb
index 6009d6524e1..44e4d29c18e 100644
--- a/spec/contracts/provider/spec_helper.rb
+++ b/spec/contracts/provider/spec_helper.rb
@@ -3,6 +3,13 @@
require 'spec_helper'
require 'zeitwerk'
require_relative 'helpers/users_helper'
+require_relative('../../../ee/spec/contracts/provider/spec_helper') if Gitlab.ee?
+require Rails.root.join("spec/support/helpers/rails_helpers.rb")
+require Rails.root.join("spec/support/helpers/stub_env.rb")
+
+# Opt out of telemetry collection. We can't allow all engineers, and users who install GitLab from source, to be
+# automatically enrolled in sending data on their usage without their knowledge.
+ENV['PACT_DO_NOT_TRACK'] = 'true'
RSpec.configure do |config|
config.include Devise::Test::IntegrationHelpers
@@ -19,6 +26,8 @@ end
Pact.configure do |config|
config.include FactoryBot::Syntax::Methods
+ config.include RailsHelpers
+ config.include StubENV
end
module SpecHelper
diff --git a/spec/contracts/provider/states/project/merge_request/show_state.rb b/spec/contracts/provider/states/project/merge_requests/show_state.rb
index 46f322f723a..ecb6784db1e 100644
--- a/spec/contracts/provider/states/project/merge_request/show_state.rb
+++ b/spec/contracts/provider/states/project/merge_requests/show_state.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-Pact.provider_states_for "MergeRequest#show" do
+Pact.provider_states_for "MergeRequests#show" do
provider_state "a merge request with diffs exists" do
set_up do
user = User.find_by(name: Provider::UsersHelper::CONTRACT_USER_NAME)
diff --git a/spec/contracts/provider/states/project/pipeline_schedule/edit_state.rb b/spec/contracts/provider/states/project/pipeline_schedules/edit_state.rb
index 4ee714f15f3..4ee714f15f3 100644
--- a/spec/contracts/provider/states/project/pipeline_schedule/edit_state.rb
+++ b/spec/contracts/provider/states/project/pipeline_schedules/edit_state.rb
diff --git a/spec/contracts/provider/states/project/pipeline/index_state.rb b/spec/contracts/provider/states/project/pipelines/index_state.rb
index 639c25e9894..639c25e9894 100644
--- a/spec/contracts/provider/states/project/pipeline/index_state.rb
+++ b/spec/contracts/provider/states/project/pipelines/index_state.rb
diff --git a/spec/contracts/provider/states/project/pipeline/new_state.rb b/spec/contracts/provider/states/project/pipelines/new_state.rb
index 95914180bec..95914180bec 100644
--- a/spec/contracts/provider/states/project/pipeline/new_state.rb
+++ b/spec/contracts/provider/states/project/pipelines/new_state.rb
diff --git a/spec/contracts/provider/states/project/pipeline/show_state.rb b/spec/contracts/provider/states/project/pipelines/show_state.rb
index 3365647cd13..3365647cd13 100644
--- a/spec/contracts/provider/states/project/pipeline/show_state.rb
+++ b/spec/contracts/provider/states/project/pipelines/show_state.rb
diff --git a/spec/contracts/provider_specs/helpers/provider/contract_source_helper_spec.rb b/spec/contracts/provider_specs/helpers/provider/contract_source_helper_spec.rb
new file mode 100644
index 00000000000..8bb3b577135
--- /dev/null
+++ b/spec/contracts/provider_specs/helpers/provider/contract_source_helper_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../../provider/helpers/contract_source_helper'
+
+RSpec.describe Provider::ContractSourceHelper, feature_category: :not_owned do
+ let(:pact_helper_path) { 'pact_helpers/project/pipelines/new/post_create_pipeline_helper.rb' }
+ let(:split_pact_helper_path) { %w[pipelines new post_create_pipeline] }
+ let(:provider_url_path) { 'POST%20create%20pipeline' }
+ let(:consumer_url_path) { 'Pipelines%23new' }
+
+ describe '#contract_location' do
+ it 'raises an error when an invalid requester is given' do
+ expect { subject.contract_location(:foo, pact_helper_path) }
+ .to raise_error(ArgumentError, 'requester must be :rake or :spec')
+ end
+
+ context 'when the PACT_BROKER environment variable is not set' do
+ it 'extracts the relevant path from the pact_helper path' do
+ expect(subject).to receive(:local_contract_location).with(:rake, split_pact_helper_path)
+
+ subject.contract_location(:rake, pact_helper_path)
+ end
+
+ it 'does not construct the pact broker url' do
+ expect(subject).not_to receive(:pact_broker_url)
+
+ subject.contract_location(:rake, pact_helper_path)
+ end
+ end
+
+ context 'when the PACT_BROKER environment variable is set' do
+ before do
+ stub_env('PACT_BROKER', true)
+ end
+
+ it 'extracts the relevant path from the pact_helper path' do
+ expect(subject).to receive(:pact_broker_url).with(split_pact_helper_path)
+
+ subject.contract_location(:spec, pact_helper_path)
+ end
+
+ it 'does not construct the pact broker url' do
+ expect(subject).not_to receive(:local_contract_location)
+
+ subject.contract_location(:spec, pact_helper_path)
+ end
+ end
+ end
+
+ describe '#pact_broker_url' do
+ it 'returns the full url to the contract that the provider test is verifying' do
+ contract_url_path = "http://localhost:9292/pacts/provider/" \
+ "#{provider_url_path}/consumer/#{consumer_url_path}/latest"
+
+ expect(subject.pact_broker_url(split_pact_helper_path)).to eq(contract_url_path)
+ end
+ end
+
+ describe '#construct_provider_url_path' do
+ it 'returns the provider url path' do
+ expect(subject.construct_provider_url_path(split_pact_helper_path)).to eq(provider_url_path)
+ end
+ end
+
+ describe '#construct_consumer_url_path' do
+ it 'returns the consumer url path' do
+ expect(subject.construct_consumer_url_path(split_pact_helper_path)).to eq(consumer_url_path)
+ end
+ end
+
+ describe '#local_contract_location' do
+ it 'returns the contract file path with the prefix path for a rake task' do
+ rake_task_relative_path = '/spec/contracts/contracts/project'
+
+ rake_task_path = subject.local_contract_location(:rake, split_pact_helper_path)
+
+ expect(rake_task_path).to include(rake_task_relative_path)
+ expect(rake_task_path).not_to include('../')
+ end
+
+ it 'returns the contract file path with the prefix path for a spec' do
+ spec_relative_path = '../contracts/project'
+
+ expect(subject.local_contract_location(:spec, split_pact_helper_path)).to include(spec_relative_path)
+ end
+ end
+
+ describe '#construct_local_contract_path' do
+ it 'returns the local contract path' do
+ contract_path = '/pipelines/new/pipelines#new-post_create_pipeline.json'
+
+ expect(subject.construct_local_contract_path(split_pact_helper_path)).to eq(contract_path)
+ end
+ end
+end
diff --git a/spec/controllers/admin/application_settings/appearances_controller_spec.rb b/spec/controllers/admin/application_settings/appearances_controller_spec.rb
index cc914f3c9b8..5978381a926 100644
--- a/spec/controllers/admin/application_settings/appearances_controller_spec.rb
+++ b/spec/controllers/admin/application_settings/appearances_controller_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe Admin::ApplicationSettings::AppearancesController do
let(:create_params) do
{
title: 'Foo',
+ short_title: 'F',
description: 'Bar',
header_message: header_message,
footer_message: footer_message
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 0ad0a111156..49c40ecee8b 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -162,6 +162,13 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
expect(ApplicationSetting.current.receive_max_input_size).to eq(1024)
end
+ it 'updates the default_preferred_language for string value' do
+ put :update, params: { application_setting: { default_preferred_language: 'zh_CN' } }
+
+ expect(response).to redirect_to(general_admin_application_settings_path)
+ expect(ApplicationSetting.current.default_preferred_language).to eq('zh_CN')
+ end
+
it 'updates the default_project_creation for string value' do
put :update, params: { application_setting: { default_project_creation: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS } }
@@ -319,6 +326,19 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
end
end
+ describe 'Terraform settings' do
+ let(:application_setting) { ApplicationSetting.current }
+
+ context 'max_terraform_state_size_bytes' do
+ it 'updates the receive_max_input_size setting' do
+ put :update, params: { application_setting: { max_terraform_state_size_bytes: '123' } }
+
+ expect(response).to redirect_to(general_admin_application_settings_path)
+ expect(application_setting.max_terraform_state_size_bytes).to eq(123)
+ end
+ end
+ end
+
describe 'user_email_lookup_limit aliasing' do
let(:application_setting) { ApplicationSetting.current }
let(:user_email_lookup_limit) { 8675 }
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index 6085f0e1239..c534cf14327 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -52,4 +52,48 @@ RSpec.describe Admin::GroupsController do
post :create, params: { group: { path: 'test', name: 'test' } }
end
end
+
+ describe 'PUT #update' do
+ subject(:update!) do
+ put :update, params: { id: group.to_param, group: { runner_registration_enabled: new_value } }
+ end
+
+ context 'with runner registration disabled' do
+ let(:runner_registration_enabled) { false }
+ let(:new_value) { '1' }
+
+ it 'updates the setting successfully' do
+ update!
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(group.reload.runner_registration_enabled).to eq(true)
+ end
+
+ it 'does not change the registration token' do
+ expect do
+ update!
+ group.reload
+ end.not_to change(group, :runners_token)
+ end
+ end
+
+ context 'with runner registration enabled' do
+ let(:runner_registration_enabled) { true }
+ let(:new_value) { '0' }
+
+ it 'updates the setting successfully' do
+ update!
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(group.reload.runner_registration_enabled).to eq(false)
+ end
+
+ it 'changes the registration token' do
+ expect do
+ update!
+ group.reload
+ end.to change(group, :runners_token)
+ end
+ end
+ end
end
diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb
index 82e4b873bf6..4101bd7f658 100644
--- a/spec/controllers/admin/hooks_controller_spec.rb
+++ b/spec/controllers/admin/hooks_controller_spec.rb
@@ -68,7 +68,7 @@ RSpec.describe Admin::HooksController do
hook.reload
expect(response).to have_gitlab_http_status(:found)
- expect(flash[:notice]).to include('successfully updated')
+ expect(flash[:notice]).to include('was updated')
expect(hook).to have_attributes(hook_params.except(:url_variables))
expect(hook).to have_attributes(
url_variables: { 'token' => 'some secret value', 'baz' => 'woo' }
diff --git a/spec/controllers/admin/plan_limits_controller_spec.rb b/spec/controllers/admin/plan_limits_controller_spec.rb
index 2666925c2b7..99795de51d8 100644
--- a/spec/controllers/admin/plan_limits_controller_spec.rb
+++ b/spec/controllers/admin/plan_limits_controller_spec.rb
@@ -29,6 +29,26 @@ RSpec.describe Admin::PlanLimitsController do
end
end
+ context "when pipeline_hierarchy_size is passed in params" do
+ let(:params) do
+ {
+ plan_limits: {
+ plan_id: plan.id,
+ pipeline_hierarchy_size: 200, id: plan_limits.id
+ }
+ }
+ end
+
+ it "updates the pipeline_hierarchy_size plan limit" do
+ sign_in(create(:admin))
+
+ post :create, params: params
+
+ expect(response).to redirect_to(general_admin_application_settings_path)
+ expect(plan_limits.reload.pipeline_hierarchy_size).to eq(params[:plan_limits][:pipeline_hierarchy_size])
+ end
+ end
+
context 'without admin access' do
let(:file_size) { 1.megabytes }
diff --git a/spec/controllers/admin/runner_projects_controller_spec.rb b/spec/controllers/admin/runner_projects_controller_spec.rb
index 98f961f66bb..38cc2d171ac 100644
--- a/spec/controllers/admin/runner_projects_controller_spec.rb
+++ b/spec/controllers/admin/runner_projects_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::RunnerProjectsController do
+RSpec.describe Admin::RunnerProjectsController, feature_category: :runner_fleet do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index 9e852cb28dd..6d58abb9d4d 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::RunnersController do
+RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
let_it_be(:runner) { create(:ci_runner) }
let_it_be(:user) { create(:admin) }
diff --git a/spec/controllers/concerns/check_rate_limit_spec.rb b/spec/controllers/concerns/check_rate_limit_spec.rb
index 34ececfe639..75776acd520 100644
--- a/spec/controllers/concerns/check_rate_limit_spec.rb
+++ b/spec/controllers/concerns/check_rate_limit_spec.rb
@@ -19,11 +19,9 @@ RSpec.describe CheckRateLimit do
@current_user = current_user
end
- def redirect_back_or_default(**args)
- end
+ def redirect_back_or_default(**args); end
- def render(**args)
- end
+ def render(**args); end
end
end
diff --git a/spec/controllers/concerns/issuable_actions_spec.rb b/spec/controllers/concerns/issuable_actions_spec.rb
index 37d9dc080e1..34f47ed16f2 100644
--- a/spec/controllers/concerns/issuable_actions_spec.rb
+++ b/spec/controllers/concerns/issuable_actions_spec.rb
@@ -14,8 +14,7 @@ RSpec.describe IssuableActions do
klass = Class.new do
attr_reader :current_user, :project, :issuable
- def self.before_action(action = nil, params = nil)
- end
+ def self.before_action(action = nil, params = nil); end
include IssuableActions
@@ -40,8 +39,7 @@ RSpec.describe IssuableActions do
[]
end
- def render(options)
- end
+ def render(options); end
end
klass.new(issuable, project, user, finder_params_for_issuable)
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index efa00877142..4f01839fec4 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -3,14 +3,12 @@
require 'spec_helper'
RSpec.describe Dashboard::TodosController do
- let(:user) { create(:user) }
- let(:author) { create(:user) }
- let(:project) { create(:project) }
- let(:todo_service) { TodoService.new }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project).tap { |project| project.add_developer(user) } }
+ let_it_be(:author) { create(:user) }
before do
sign_in(user)
- project.add_developer(user)
end
describe 'GET #index' do
@@ -83,11 +81,14 @@ RSpec.describe Dashboard::TodosController do
end
it_behaves_like 'paginated collection' do
- let!(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
+ let_it_be(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
let(:collection) { user.todos }
+ before_all do
+ issues.each { |issue| TodoService.new.new_issue(issue, user) }
+ end
+
before do
- issues.each { |issue| todo_service.new_issue(issue, user) }
allow(Kaminari.config).to receive(:default_per_page).and_return(2)
end
@@ -126,6 +127,26 @@ RSpec.describe Dashboard::TodosController do
expect(assigns(:todos)).to match_array(mentioned_todos)
end
+
+ context 'when filtering by type Issue' do
+ it 'also includes work item todos' do
+ mentioned_issue_todos = [
+ create(:todo, :mentioned, project: project, user: user, target: issues.first),
+ create(
+ :todo,
+ :mentioned,
+ project: project,
+ user: user,
+ target_id: issues.last.id,
+ target_type: 'WorkItem'
+ )
+ ]
+
+ get :index, params: { action_id: ::Todo::MENTIONED, type: 'Issue', project_id: project.id }
+
+ expect(assigns(:todos)).to match_array(mentioned_issue_todos)
+ end
+ end
end
end
diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb
index 5c977439af4..a79d9fa1276 100644
--- a/spec/controllers/explore/projects_controller_spec.rb
+++ b/spec/controllers/explore/projects_controller_spec.rb
@@ -101,6 +101,7 @@ RSpec.describe Explore::ProjectsController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
context 'when topic exists' do
before do
create(:topic, name: 'topic1')
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index fe8b0291733..75f281caa90 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -47,6 +47,23 @@ RSpec.describe GraphqlController do
'raisedAt' => /graphql_controller_spec.rb/))
)
end
+
+ it 'handles Gitlab::Auth::TooManyIps', :aggregate_failures do
+ allow(controller).to receive(:execute) do
+ raise Gitlab::Auth::TooManyIps.new(150, '123.123.123.123', 10)
+ end
+
+ expect(controller).to receive(:log_exception).and_call_original
+
+ post :execute
+
+ expect(json_response).to include(
+ 'errors' => include(
+ a_hash_including('message' => 'User 150 from IP: 123.123.123.123 tried logging from too many ips: 10')
+ )
+ )
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
end
describe 'POST #execute' do
@@ -191,7 +208,7 @@ RSpec.describe GraphqlController do
expected_message = "Authentication error: " \
"enable 2FA in your profile settings to continue using GitLab: %{mfa_help_page}" %
- { mfa_help_page: EnforcesTwoFactorAuthentication::MFA_HELP_PAGE }
+ { mfa_help_page: controller.mfa_help_page_url }
expect(json_response).to eq({ 'errors' => [{ 'message' => expected_message }] })
end
diff --git a/spec/controllers/groups/application_controller_spec.rb b/spec/controllers/groups/application_controller_spec.rb
new file mode 100644
index 00000000000..46908fdb26a
--- /dev/null
+++ b/spec/controllers/groups/application_controller_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::ApplicationController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ describe '#respond_to_missing?' do
+ it 'returns true if the method matches the name structure' do
+ expect(controller.respond_to?(:authorize_read_usage_quotas!)).to eq(true)
+ end
+
+ it 'returns false if the method does not match the name structure' do
+ expect(controller.respond_to?(:does_not_exist)).to eq(false)
+ end
+ end
+
+ describe '#method_missing' do
+ controller do
+ before_action :authorize_read_usage_quotas!
+
+ def index
+ head :ok
+ end
+ end
+
+ it 'calls authorize_action! with the policy and renders not_found when user not authorized' do
+ group.add_maintainer(user)
+ sign_in(user)
+ get :index, params: { group_id: group.to_param }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.headers['X-GitLab-Custom-Error']).to eq '1'
+ end
+
+ it 'calls authorize_action! with the policy and renders OK when user is authorized' do
+ group.add_owner(user)
+ sign_in(user)
+ get :index, params: { group_id: group.to_param }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+end
diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
index 5b4b00106cb..f1ca9e11a1a 100644
--- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
+++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
@@ -233,7 +233,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
it_behaves_like 'a successful manifest pull'
- it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
+ it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest', false
context 'with workhorse response' do
let(:pull_response) { { status: :success, manifest: nil, from_cache: false } }
@@ -303,7 +303,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
it_behaves_like 'a successful blob pull'
- it_behaves_like 'a package tracking event', described_class.name, 'pull_blob_from_cache'
+ it_behaves_like 'a package tracking event', described_class.name, 'pull_blob_from_cache', false
context 'when cache entry does not exist' do
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
@@ -387,7 +387,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
group.add_guest(user)
end
- it_behaves_like 'a package tracking event', described_class.name, 'pull_blob'
+ it_behaves_like 'a package tracking event', described_class.name, 'pull_blob', false
it 'creates a blob' do
expect { subject }.to change { group.dependency_proxy_blobs.count }.by(1)
@@ -445,7 +445,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
group.add_guest(user)
end
- it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
+ it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest', false
it_behaves_like 'with invalid path'
context 'with no existing manifest' do
diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb
index 37db26096d3..0521c5e02a8 100644
--- a/spec/controllers/groups/labels_controller_spec.rb
+++ b/spec/controllers/groups/labels_controller_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe Groups::LabelsController do
get :index, params: { group_id: group.to_param }
end
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :use_clean_rails_redis_caching do
control = ActiveRecord::QueryRecorder.new(skip_cached: false) { get :index, params: { group_id: group.to_param } }
create_list(:group_label, 3, group: group)
diff --git a/spec/controllers/groups/registry/repositories_controller_spec.rb b/spec/controllers/groups/registry/repositories_controller_spec.rb
index 62c15201a95..efdafcd2657 100644
--- a/spec/controllers/groups/registry/repositories_controller_spec.rb
+++ b/spec/controllers/groups/registry/repositories_controller_spec.rb
@@ -114,7 +114,7 @@ RSpec.describe Groups::Registry::RepositoriesController do
it_behaves_like 'with name parameter'
- it_behaves_like 'a package tracking event', described_class.name, 'list_repositories'
+ it_behaves_like 'a package tracking event', described_class.name, 'list_repositories', false
context 'with project in subgroup' do
let_it_be(:test_group) { create(:group, parent: group) }
diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb
index 2add3cd3b18..93c1571bb6c 100644
--- a/spec/controllers/groups/runners_controller_spec.rb
+++ b/spec/controllers/groups/runners_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::RunnersController do
+RSpec.describe Groups::RunnersController, feature_category: :runner_fleet 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/controllers/groups/settings/repository_controller_spec.rb b/spec/controllers/groups/settings/repository_controller_spec.rb
index 73a205069f5..ab1bb134b03 100644
--- a/spec/controllers/groups/settings/repository_controller_spec.rb
+++ b/spec/controllers/groups/settings/repository_controller_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Groups::Settings::RepositoryController do
let(:good_deploy_token_params) do
{
name: 'name',
- expires_at: 1.day.from_now.to_s,
+ expires_at: 1.day.from_now.to_datetime.to_s,
username: 'deployer',
read_repository: '1',
deploy_token_type: DeployToken.deploy_token_types[:group_type]
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index e73e61b6ec5..35f712dc50d 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -185,6 +185,14 @@ RSpec.describe Import::BitbucketController do
post :create, format: :json
+ expect_snowplow_event(
+ category: 'Import::BitbucketController',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Owner', import_type: 'bitbucket' }
+ )
+
expect(response).to have_gitlab_http_status(:ok)
end
@@ -297,6 +305,14 @@ RSpec.describe Import::BitbucketController do
.to receive(:new).and_return(double(execute: project))
expect { post :create, format: :json }.not_to change(Namespace, :count)
+
+ expect_snowplow_event(
+ category: 'Import::BitbucketController',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Owner', import_type: 'bitbucket' }
+ )
end
it "takes the current user's namespace" do
@@ -417,6 +433,14 @@ RSpec.describe Import::BitbucketController do
post :create, params: { target_namespace: other_namespace.name }, format: :json
expect(response).to have_gitlab_http_status(:unprocessable_entity)
+
+ expect_snowplow_event(
+ category: 'Import::BitbucketController',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Not a member', import_type: 'bitbucket' }
+ )
end
end
end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index f3632e7370c..a85af89b262 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -41,6 +41,16 @@ RSpec.describe Import::GithubController do
expect(response).to render_template(:new)
end
end
+
+ it 'gets authorization url using oauth client' do
+ allow(controller).to receive(:logged_in_with_provider?).and_return(true)
+ expect(controller).to receive(:go_to_provider_for_permissions).and_call_original
+ expect_next_instance_of(OAuth2::Client) do |client|
+ expect(client.auth_code).to receive(:authorize_url).and_call_original
+ end
+
+ get :new
+ end
end
describe "GET callback" do
@@ -124,7 +134,48 @@ RSpec.describe Import::GithubController do
end
describe "GET status" do
- context 'when using OAuth' do
+ shared_examples 'calls repos through Clients::Proxy with expected args' do
+ it 'calls repos list from provider with expected args' do
+ expect_next_instance_of(Gitlab::GithubImport::Clients::Proxy) do |client|
+ expect(client).to receive(:repos)
+ .with(expected_filter, expected_pagination_options)
+ .and_return({ repos: [], page_info: {} })
+ end
+
+ get :status, params: params, format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['imported_projects'].size).to eq 0
+ expect(json_response['provider_repos'].size).to eq 0
+ expect(json_response['incompatible_repos'].size).to eq 0
+ expect(json_response['page_info']).to eq({})
+ end
+ end
+
+ let(:provider_token) { 'asdasd12345' }
+ let(:client_auth_success) { true }
+ let(:client_stub) { instance_double(Gitlab::GithubImport::Client, user: { login: 'user' }) }
+ let(:expected_pagination_options) { pagination_params.merge(first: 25, page: 1, per_page: 25) }
+ let(:expected_filter) { nil }
+ let(:params) { nil }
+ let(:pagination_params) { { before: nil, after: nil } }
+ let(:provider_repos) { [] }
+
+ before do
+ allow_next_instance_of(Gitlab::GithubImport::Clients::Proxy) do |proxy|
+ if client_auth_success
+ allow(proxy).to receive(:repos).and_return({ repos: provider_repos })
+ allow(proxy).to receive(:client).and_return(client_stub)
+ else
+ allow(proxy).to receive(:repos).and_raise(Octokit::Unauthorized)
+ end
+ end
+ session[:"#{provider}_access_token"] = provider_token
+ end
+
+ context 'with OAuth' do
+ let(:provider_token) { nil }
+
before do
allow(controller).to receive(:logged_in_with_provider?).and_return(true)
end
@@ -146,178 +197,133 @@ RSpec.describe Import::GithubController do
end
end
- context 'when feature remove_legacy_github_client is disabled' do
- before do
- stub_feature_flags(remove_legacy_github_client: false)
- session[:"#{provider}_access_token"] = 'asdasd12345'
- end
+ context 'with invalid access token' do
+ let(:client_auth_success) { false }
- it_behaves_like 'a GitHub-ish import controller: GET status'
+ it "handles an invalid token" do
+ get :status, format: :json
- it 'uses Gitlab::LegacyGitHubImport::Client' do
- expect(controller.send(:client)).to be_instance_of(Gitlab::LegacyGithubImport::Client)
+ expect(session[:"#{provider}_access_token"]).to be_nil
+ expect(controller).to redirect_to(new_import_url)
+ expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.")
end
+ end
- it 'fetches repos using legacy client' do
- expect_next_instance_of(Gitlab::LegacyGithubImport::Client) do |client|
- expect(client).to receive(:repos).and_return([])
- end
+ context 'when user has few different repos' do
+ let(:repo_struct) { Struct.new(:id, :login, :full_name, :name, :owner, keyword_init: true) }
+ let(:provider_repos) do
+ [repo_struct.new(login: 'vim', full_name: 'asd/vim', name: 'vim', owner: { login: 'owner' })]
+ end
- get :status
+ let!(:imported_project) do
+ create(
+ :project,
+ import_type: provider, namespace: user.namespace,
+ import_status: :finished, import_source: 'example/repo'
+ )
end
- it 'gets authorization url using legacy client' do
- allow(controller).to receive(:logged_in_with_provider?).and_return(true)
- expect(controller).to receive(:go_to_provider_for_permissions).and_call_original
- expect_next_instance_of(Gitlab::LegacyGithubImport::Client) do |client|
- expect(client).to receive(:authorize_url).and_call_original
- end
+ it 'responds with expected high-level structure' do
+ get :status, format: :json
- get :new
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.dig("imported_projects", 0, "id")).to eq(imported_project.id)
+ expect(json_response.dig("provider_repos", 0, "id")).to eq(provider_repos[0].id)
end
end
- context 'when feature remove_legacy_github_client is enabled' do
+ it_behaves_like 'calls repos through Clients::Proxy with expected args'
+
+ context 'with namespace_id param' do
+ let_it_be(:user) { create(:user) }
+
before do
- stub_feature_flags(remove_legacy_github_client: true)
- session[:"#{provider}_access_token"] = 'asdasd12345'
+ sign_in(user)
end
- it_behaves_like 'a GitHub-ish import controller: GET status'
-
- it 'uses Gitlab::GithubImport::Client' do
- expect(controller.send(:client)).to be_instance_of(Gitlab::GithubImport::Client)
+ after do
+ sign_out(user)
end
- it 'fetches repos using latest github client' do
- expect_next_instance_of(Gitlab::GithubImport::Client) do |client|
- expect(client).to receive(:repos).and_return([])
- end
+ context 'when user is allowed to create projects in this namespace' do
+ let(:namespace) { create(:namespace, owner: user) }
- get :status
- end
+ it 'provides namespace to the template' do
+ get :status, params: { namespace_id: namespace.id }, format: :html
- it 'gets authorization url using oauth client' do
- allow(controller).to receive(:logged_in_with_provider?).and_return(true)
- expect(controller).to receive(:go_to_provider_for_permissions).and_call_original
- expect_next_instance_of(OAuth2::Client) do |client|
- expect(client.auth_code).to receive(:authorize_url).and_call_original
+ expect(response).to have_gitlab_http_status :ok
+ expect(assigns(:namespace)).to eq(namespace)
end
-
- get :new
end
- context 'pagination' do
- context 'when no page is specified' do
- it 'requests first page' do
- expect_next_instance_of(Gitlab::GithubImport::Client) do |client|
- expect(client).to receive(:repos).with({ page: 1, per_page: 25 }).and_return([])
- end
-
- get :status
- end
- end
+ context 'when user is not allowed to create projects in this namespace' do
+ let(:namespace) { create(:namespace) }
- context 'when page is specified' do
- it 'requests repos with specified page' do
- expect_next_instance_of(Octokit::Client) do |client|
- expect(client).to receive(:repos).with(nil, { page: 2, per_page: 25 }).and_return([].to_enum)
- end
+ it 'renders 404' do
+ get :status, params: { namespace_id: namespace.id }, format: :html
- get :status, params: { page: 2 }
- end
+ expect(response).to have_gitlab_http_status :not_found
end
end
+ end
- context 'when filtering' do
- let(:filter) { 'test' }
- let(:user_login) { 'user' }
- let(:collaborations_subquery) { 'repo:repo1 repo:repo2' }
- let(:organizations_subquery) { 'org:org1 org:org2' }
- let(:search_query) { "test in:name is:public,private user:#{user_login} #{collaborations_subquery} #{organizations_subquery}" }
-
- before do
- allow_next_instance_of(Octokit::Client) do |client|
- allow(client).to receive(:user).and_return(double(login: user_login))
- end
- end
+ context 'pagination' do
+ context 'when cursor is specified' do
+ let(:pagination_params) { { before: nil, after: 'CURSOR' } }
+ let(:params) { pagination_params }
- it 'makes request to github search api' do
- expect_next_instance_of(Octokit::Client) do |client|
- expect(client).to receive(:user).and_return({ login: user_login })
- expect(client).to receive(:search_repositories).with(search_query, { page: 1, per_page: 25 }).and_return({ items: [].to_enum })
- end
+ it_behaves_like 'calls repos through Clients::Proxy with expected args'
+ end
- expect_next_instance_of(Gitlab::GithubImport::Client) do |client|
- expect(client).to receive(:collaborations_subquery).and_return(collaborations_subquery)
- expect(client).to receive(:organizations_subquery).and_return(organizations_subquery)
- end
+ context 'when page is specified' do
+ let(:pagination_params) { { before: nil, after: nil, page: 2 } }
+ let(:expected_pagination_options) { pagination_params.merge(first: 25, page: 2, per_page: 25) }
+ let(:params) { pagination_params }
- get :status, params: { filter: filter }, format: :json
- end
+ it_behaves_like 'calls repos through Clients::Proxy with expected args'
+ end
+ end
- context 'pagination' do
- context 'when no page is specified' do
- it 'requests first page' do
- expect_next_instance_of(Octokit::Client) do |client|
- expect(client).to receive(:user).and_return({ login: user_login })
- expect(client).to receive(:search_repositories).with(search_query, { page: 1, per_page: 25 }).and_return({ items: [].to_enum })
- end
-
- expect_next_instance_of(Gitlab::GithubImport::Client) do |client|
- expect(client).to receive(:collaborations_subquery).and_return(collaborations_subquery)
- expect(client).to receive(:organizations_subquery).and_return(organizations_subquery)
- end
-
- get :status, params: { filter: filter }, format: :json
- end
- end
+ context 'when filtering' do
+ let(:filter_param) { FFaker::Lorem.word }
+ let(:params) { { filter: filter_param } }
+ let(:expected_filter) { filter_param }
- context 'when page is specified' do
- it 'requests repos with specified page' do
- expect_next_instance_of(Octokit::Client) do |client|
- expect(client).to receive(:user).and_return({ login: user_login })
- expect(client).to receive(:search_repositories).with(search_query, { page: 2, per_page: 25 }).and_return({ items: [].to_enum })
- end
+ it_behaves_like 'calls repos through Clients::Proxy with expected args'
- expect_next_instance_of(Gitlab::GithubImport::Client) do |client|
- expect(client).to receive(:collaborations_subquery).and_return(collaborations_subquery)
- expect(client).to receive(:organizations_subquery).and_return(organizations_subquery)
- end
+ context 'with pagination' do
+ context 'when before cursor present' do
+ let(:pagination_params) { { before: 'before-cursor', after: nil } }
+ let(:params) { { filter: filter_param }.merge(pagination_params) }
- get :status, params: { filter: filter, page: 2 }, format: :json
- end
- end
+ it_behaves_like 'calls repos through Clients::Proxy with expected args'
end
- context 'when user input contains colons and spaces' do
- before do
- allow_next_instance_of(Gitlab::GithubImport::Client) do |client|
- allow(client).to receive(:search_repos_by_name).and_return(items: [])
- end
- end
+ context 'when after cursor present' do
+ let(:pagination_params) { { before: nil, after: 'after-cursor' } }
+ let(:params) { { filter: filter_param }.merge(pagination_params) }
- it 'sanitizes user input' do
- filter = ' test1:test2 test3 : test4 '
- expected_filter = 'test1test2test3test4'
+ it_behaves_like 'calls repos through Clients::Proxy with expected args'
+ end
+ end
- get :status, params: { filter: filter }, format: :json
+ context 'when user input contains colons and spaces' do
+ let(:filter_param) { ' test1:test2 test3 : test4 ' }
+ let(:expected_filter) { 'test1test2test3test4' }
- expect(assigns(:filter)).to eq(expected_filter)
- end
- end
+ it_behaves_like 'calls repos through Clients::Proxy with expected args'
+ end
+ end
- context 'when rate limit threshold is exceeded' do
- before do
- allow(controller).to receive(:status).and_raise(Gitlab::GithubImport::RateLimitError)
- end
+ context 'when rate limit threshold is exceeded' do
+ before do
+ allow(controller).to receive(:status).and_raise(Gitlab::GithubImport::RateLimitError)
+ end
- it 'returns 429' do
- get :status, params: { filter: 'test' }, format: :json
+ it 'returns 429' do
+ get :status, format: :json
- expect(response).to have_gitlab_http_status(:too_many_requests)
- end
- end
+ expect(response).to have_gitlab_http_status(:too_many_requests)
end
end
end
@@ -331,12 +337,12 @@ RSpec.describe Import::GithubController do
describe "GET realtime_changes" do
let(:user) { create(:user) }
- it_behaves_like 'a GitHub-ish import controller: GET realtime_changes'
-
before do
assign_session_token(provider)
end
+ it_behaves_like 'a GitHub-ish import controller: GET realtime_changes'
+
it 'includes stats in response' do
create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo')
diff --git a/spec/controllers/jira_connect/events_controller_spec.rb b/spec/controllers/jira_connect/events_controller_spec.rb
index 80375a02b33..7da9eb7ac16 100644
--- a/spec/controllers/jira_connect/events_controller_spec.rb
+++ b/spec/controllers/jira_connect/events_controller_spec.rb
@@ -101,6 +101,14 @@ RSpec.describe JiraConnect::EventsController do
expect(response).to have_gitlab_http_status(:ok)
end
+ it 'uses the JiraConnectInstallations::UpdateService' do
+ expect_next_instance_of(JiraConnectInstallations::UpdateService, installation, anything) do |update_service|
+ expect(update_service).to receive(:execute).and_call_original
+ end
+
+ subject
+ end
+
context 'when parameters include a new shared secret and base_url' do
let(:shared_secret) { 'new_secret' }
let(:base_url) { 'https://new_test.atlassian.net' }
@@ -125,6 +133,36 @@ RSpec.describe JiraConnect::EventsController do
end
end
end
+
+ shared_examples 'generates JWT validation claims' do
+ specify do
+ expect_next_instance_of(Atlassian::JiraConnect::Jwt::Asymmetric, anything, expected_claims) do |asymmetric_jwt|
+ allow(asymmetric_jwt).to receive(:valid?).and_return(true)
+ end
+
+ subject
+ end
+ end
+
+ context 'when enforce_jira_base_url_https' do
+ before do
+ allow(Gitlab.config.jira_connect).to receive(:enforce_jira_base_url_https).and_return(true)
+ end
+
+ let(:expected_claims) { { aud: "https://test.host/-/jira_connect", iss: anything, qsh: anything } }
+
+ it_behaves_like 'generates JWT validation claims'
+ end
+
+ context 'when not enforce_jira_base_url_https' do
+ before do
+ allow(Gitlab.config.jira_connect).to receive(:enforce_jira_base_url_https).and_return(false)
+ end
+
+ let(:expected_claims) { { aud: "http://test.host/-/jira_connect", iss: anything, qsh: anything } }
+
+ it_behaves_like 'generates JWT validation claims'
+ end
end
describe '#uninstalled' do
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 0560ccb25dd..ab3f3fd397d 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -391,6 +391,32 @@ RSpec.describe OmniauthCallbacksController, type: :controller do
end
end
end
+
+ context 'with snowplow tracking', :snowplow do
+ let(:provider) { 'google_oauth2' }
+ let(:extern_uid) { 'my-uid' }
+
+ context 'when sign_in' do
+ it 'does not track the event' do
+ post provider
+ expect_no_snowplow_event
+ end
+ end
+
+ context 'when sign_up' do
+ let(:user) { double(email: generate(:email)) }
+
+ it 'tracks the event' do
+ post provider
+
+ expect_snowplow_event(
+ category: described_class.name,
+ action: "#{provider}_sso",
+ user: User.find_by(email: user.email)
+ )
+ end
+ end
+ end
end
describe '#saml' do
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index 63818337722..ed9022faf1b 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -14,13 +14,14 @@ RSpec.describe Profiles::KeysController do
expires_at = 3.days.from_now
expect do
- post :create, params: { key: build(:key, expires_at: expires_at).attributes }
+ post :create, params: { key: build(:key, usage_type: :signing, expires_at: expires_at).attributes }
end.to change { Key.count }.by(1)
key = Key.last
expect(key.expires_at).to be_like_time(expires_at)
expect(key.fingerprint_md5).to be_present
expect(key.fingerprint_sha256).to be_present
+ expect(key.usage_type).to eq('signing')
end
context 'with FIPS mode', :fips_mode do
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 5927f20df97..2334521b8a8 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -91,6 +91,34 @@ RSpec.describe Projects::EnvironmentsController do
expect(json_response['stopped_count']).to eq 1
end
+ it 'supports search within environment folder name' do
+ create(:environment, project: project, name: 'review-app', state: :available)
+
+ get :index, params: environment_params(format: :json, search: 'review')
+
+ expect(environments.map { |env| env['name'] }).to contain_exactly('review-app',
+ 'staging/review-1',
+ 'staging/review-2')
+ expect(json_response['available_count']).to eq 3
+ expect(json_response['stopped_count']).to eq 1
+ end
+
+ context 'when enable_environments_search_within_folder FF is disabled' do
+ before do
+ stub_feature_flags(enable_environments_search_within_folder: false)
+ end
+
+ it 'ignores name inside folder' do
+ create(:environment, project: project, name: 'review-app', state: :available)
+
+ get :index, params: environment_params(format: :json, search: 'review')
+
+ expect(environments.map { |env| env['name'] }).to contain_exactly('review-app')
+ expect(json_response['available_count']).to eq 1
+ expect(json_response['stopped_count']).to eq 0
+ end
+ end
+
it 'sets the polling interval header' do
subject
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index 3dfc22927cf..1e9d999311a 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -11,6 +11,50 @@ RSpec.describe Projects::GraphsController do
project.add_maintainer(user)
end
+ describe '#show' do
+ subject { get(:show, params: params) }
+
+ let(:params) { { namespace_id: project.namespace.path, project_id: project.path, id: 'master' } }
+
+ describe 'ref_type' do
+ it 'assigns ref_type' do
+ subject
+
+ expect(assigns[:languages]).to be_nil
+ end
+
+ context 'when ref_type is provided' do
+ before do
+ params[:ref_type] = 'heads'
+ end
+
+ it 'assigns ref_type' do
+ subject
+
+ expect(assigns[:ref_type]).to eq('heads')
+ end
+ end
+ end
+
+ describe 'when format is json' do
+ let(:stubbed_limit) { 1 }
+
+ before do
+ params[:format] = 'json'
+ stub_const('Projects::GraphsController::MAX_COMMITS', stubbed_limit)
+ end
+
+ it 'renders json' do
+ subject
+
+ expect(json_response.size).to eq(stubbed_limit)
+ %w[author_name author_email date].each do |key|
+ expect(json_response[0]).to have_key(key)
+ end
+ end
+ end
+ end
+
describe 'GET languages' do
it "redirects_to action charts" do
get(:commits, params: { namespace_id: project.namespace.path, project_id: project.path, id: 'master' })
diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb
index 18f16937505..815370d428d 100644
--- a/spec/controllers/projects/hooks_controller_spec.rb
+++ b/spec/controllers/projects/hooks_controller_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe Projects::HooksController do
put :update, params: params
expect(response).to have_gitlab_http_status(:found)
- expect(flash[:notice]).to include('successfully updated')
+ expect(flash[:notice]).to include('was updated')
expect(hook.reload.url_variables).to eq(
'a' => 'updated',
@@ -154,28 +154,6 @@ RSpec.describe Projects::HooksController do
expect(flash[:alert]).to be_blank
end
- it 'ignores branch_filter_strategy when flag is disabled' do
- stub_feature_flags(enhanced_webhook_support_regex: false)
- hook_params = {
- url: 'http://example.com',
- branch_filter_strategy: 'regex',
- push_events: true
- }
- params = { namespace_id: project.namespace, project_id: project, hook: hook_params }
-
- expect { post :create, params: params }.to change(ProjectHook, :count).by(1)
-
- project_hook = ProjectHook.order_id_desc.take
-
- expect(project_hook).to have_attributes(
- url: 'http://example.com',
- branch_filter_strategy: 'wildcard'
- )
-
- expect(response).to have_gitlab_http_status(:found)
- expect(flash[:alert]).to be_blank
- end
-
it 'alerts the user if the new hook is invalid' do
hook_params = {
token: "TEST\nTOKEN",
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 8f26be442a7..31e297e5773 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -141,18 +141,32 @@ RSpec.describe Projects::IssuesController do
project.add_developer(user)
end
- it "returns issue attributes" do
- participants = create_list(:issue_email_participant, 2, issue: issue)
+ context 'issue email participants' do
+ context 'when issue is confidential' do
+ let(:issue) { create(:issue, project: project, confidential: true) }
+ let!(:participants) { create_list(:issue_email_participant, 2, issue: issue) }
- get :show, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }, format: :json
+ it "returns issue email participants" do
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }, format: :json
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to include(
- 'issue_email_participants' => contain_exactly(
- { "email" => participants[0].email }, { "email" => participants[1].email }
- ),
- 'type' => 'ISSUE'
- )
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ 'issue_email_participants' => contain_exactly(
+ { "email" => participants[0].email }, { "email" => participants[1].email }
+ ),
+ 'type' => 'ISSUE'
+ )
+ end
+ end
+
+ context 'when issue is not confidential' do
+ it "returns empty email participants" do
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }, format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include('issue_email_participants' => [])
+ end
+ end
end
context 'when issue is not a task and work items feature flag is enabled' do
@@ -366,10 +380,10 @@ RSpec.describe Projects::IssuesController do
}
end
- context 'the current user cannot download code' do
+ context 'the current user cannot read code' do
it 'prevents access' do
allow(controller).to receive(:can?).with(any_args).and_return(true)
- allow(controller).to receive(:can?).with(user, :download_code, project).and_return(false)
+ allow(controller).to receive(:can?).with(user, :read_code, project).and_return(false)
subject
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index a5259522fe2..dfa6ed639b6 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -100,7 +100,7 @@ RSpec.describe Projects::LabelsController do
list_labels
end
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :use_clean_rails_redis_caching do
control = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_labels }
create_list(:label, 3, project: project)
diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
index 366a1e587ab..311af26abf6 100644
--- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
@@ -105,15 +105,13 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
if section['conflict']
expect(line['type']).to be_in(%w(old new))
expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer))
+ elsif line['type'].nil?
+ expect(line['old_line']).not_to eq(nil)
+ expect(line['new_line']).not_to eq(nil)
else
- if line['type'].nil?
- expect(line['old_line']).not_to eq(nil)
- expect(line['new_line']).not_to eq(nil)
- else
- expect(line['type']).to eq('match')
- expect(line['old_line']).to eq(nil)
- expect(line['new_line']).to eq(nil)
- end
+ expect(line['type']).to eq('match')
+ expect(line['old_line']).to eq(nil)
+ expect(line['new_line']).to eq(nil)
end
end
end
diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
index a061a14c7b1..ace8c04b819 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -306,4 +306,18 @@ RSpec.describe Projects::MergeRequests::CreationsController do
post :create, params: merge_request_params
end
end
+
+ describe 'GET target_projects', feature_category: :code_review do
+ it 'returns target projects JSON' do
+ get :target_projects, params: { namespace_id: project.namespace.to_param, project_id: project }
+
+ expect(json_response.size).to be(2)
+
+ forked_project = json_response.first
+ expect(forked_project).to have_key('id')
+ expect(forked_project).to have_key('name')
+ expect(forked_project).to have_key('full_path')
+ expect(forked_project).to have_key('refs_url')
+ end
+ end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 026cf19bde5..a93dc806283 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MergeRequestsController do
+RSpec.describe Projects::MergeRequestsController, feature_category: :code_review do
include ProjectForksHelper
include Gitlab::Routing
using RSpec::Parameterized::TableSyntax
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 1f8e96258ca..0afd2e10ea2 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -150,7 +150,7 @@ RSpec.describe Projects::NotesController do
context 'when user cannot read commit' do
before do
allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(user, :download_code, project).and_return(false)
+ allow(Ability).to receive(:allowed?).with(user, :read_code, project).and_return(false)
end
it 'renders 404' do
@@ -757,6 +757,7 @@ RSpec.describe Projects::NotesController do
expect { put :update, params: request_params }.to change { note.reload.note }
end
end
+
context "doesnt update the note" do
let(:issue) { create(:issue, :confidential, project: project) }
let(:note) { create(:note, noteable: issue, project: project) }
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index 5bcfae4227c..358b98621a5 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -259,7 +259,7 @@ RSpec.describe Projects::PipelineSchedulesController do
context 'when adds a new duplicated variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'CCC', secret_value: 'AAA123' }]
+ variables_attributes: [{ key: 'dup_key', secret_value: 'value_one' }, { key: 'dup_key', secret_value: 'value_two' }]
})
end
@@ -302,7 +302,7 @@ RSpec.describe Projects::PipelineSchedulesController do
let(:schedule) do
basic_param.merge({
variables_attributes: [{ id: pipeline_schedule_variable.id, _destroy: true },
- { key: 'CCC', secret_value: 'CCC123' }]
+ { key: 'AAA', secret_value: 'AAA123' }]
})
end
@@ -310,8 +310,8 @@ RSpec.describe Projects::PipelineSchedulesController do
expect { go }.not_to change { Ci::PipelineScheduleVariable.count }
pipeline_schedule.reload
- expect(pipeline_schedule.variables.last.key).to eq('CCC')
- expect(pipeline_schedule.variables.last.value).to eq('CCC123')
+ expect(pipeline_schedule.variables.last.key).to eq('AAA')
+ expect(pipeline_schedule.variables.last.value).to eq('AAA123')
end
end
end
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index 56415663109..a7a8361ae20 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -2,15 +2,89 @@
require 'spec_helper'
-RSpec.describe Projects::RefsController do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
+RSpec.describe Projects::RefsController, feature_category: :source_code_management do
+ let_it_be(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
before do
sign_in(user)
project.add_developer(user)
end
+ describe 'GET #switch' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:id) { 'master' }
+ let(:params) do
+ { destination: destination, namespace_id: project.namespace.to_param, project_id: project, id: id,
+ ref_type: ref_type }
+ end
+
+ subject { get :switch, params: params }
+
+ context 'when the use_ref_type_parameter feature flag is not enabled' do
+ before do
+ stub_feature_flags(use_ref_type_parameter: false)
+ end
+
+ where(:destination, :ref_type, :redirected_to) do
+ 'tree' | nil | lazy { project_tree_path(project, id) }
+ 'tree' | 'heads' | lazy { project_tree_path(project, id) }
+ 'blob' | nil | lazy { project_blob_path(project, id) }
+ 'blob' | 'heads' | lazy { project_blob_path(project, id) }
+ 'graph' | nil | lazy { project_network_path(project, id) }
+ 'graph' | 'heads' | lazy { project_network_path(project, id) }
+ 'graphs' | nil | lazy { project_graph_path(project, id) }
+ 'graphs' | 'heads' | lazy { project_graph_path(project, id) }
+ 'find_file' | nil | lazy { project_find_file_path(project, id) }
+ 'find_file' | 'heads' | lazy { project_find_file_path(project, id) }
+ 'graphs_commits' | nil | lazy { commits_project_graph_path(project, id) }
+ 'graphs_commits' | 'heads' | lazy { commits_project_graph_path(project, id) }
+ 'badges' | nil | lazy { project_settings_ci_cd_path(project, ref: id) }
+ 'badges' | 'heads' | lazy { project_settings_ci_cd_path(project, ref: id) }
+ 'commits' | nil | lazy { project_commits_path(project, id) }
+ 'commits' | 'heads' | lazy { project_commits_path(project, id) }
+ 'somethingelse' | nil | lazy { project_commits_path(project, id) }
+ 'somethingelse' | 'heads' | lazy { project_commits_path(project, id) }
+ end
+
+ with_them do
+ it 'redirects to destination' do
+ expect(subject).to redirect_to(redirected_to)
+ end
+ end
+ end
+
+ context 'when the use_ref_type_parameter feature flag is enabled' do
+ where(:destination, :ref_type, :redirected_to) do
+ 'tree' | nil | lazy { project_tree_path(project, id) }
+ 'tree' | 'heads' | lazy { project_tree_path(project, id) }
+ 'blob' | nil | lazy { project_blob_path(project, id) }
+ 'blob' | 'heads' | lazy { project_blob_path(project, id) }
+ 'graph' | nil | lazy { project_network_path(project, id) }
+ 'graph' | 'heads' | lazy { project_network_path(project, id, ref_type: 'heads') }
+ 'graphs' | nil | lazy { project_graph_path(project, id) }
+ 'graphs' | 'heads' | lazy { project_graph_path(project, id, ref_type: 'heads') }
+ 'find_file' | nil | lazy { project_find_file_path(project, id) }
+ 'find_file' | 'heads' | lazy { project_find_file_path(project, id) }
+ 'graphs_commits' | nil | lazy { commits_project_graph_path(project, id) }
+ 'graphs_commits' | 'heads' | lazy { commits_project_graph_path(project, id) }
+ 'badges' | nil | lazy { project_settings_ci_cd_path(project, ref: id) }
+ 'badges' | 'heads' | lazy { project_settings_ci_cd_path(project, ref: id) }
+ 'commits' | nil | lazy { project_commits_path(project, id) }
+ 'commits' | 'heads' | lazy { project_commits_path(project, id, ref_type: 'heads') }
+ nil | nil | lazy { project_commits_path(project, id) }
+ nil | 'heads' | lazy { project_commits_path(project, id, ref_type: 'heads') }
+ end
+
+ with_them do
+ it 'redirects to destination' do
+ expect(subject).to redirect_to(redirected_to)
+ end
+ end
+ end
+ end
+
describe 'GET #logs_tree' do
let(:path) { 'foo/bar/baz.html' }
diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb
index f4f5c182850..59bc1ba04e7 100644
--- a/spec/controllers/projects/registry/repositories_controller_spec.rb
+++ b/spec/controllers/projects/registry/repositories_controller_spec.rb
@@ -120,22 +120,6 @@ RSpec.describe Projects::Registry::RepositoriesController do
expect_snowplow_event(category: anything, action: 'delete_repository')
end
-
- context 'with container_registry_delete_repository_with_cron_worker disabled' do
- before do
- stub_feature_flags(container_registry_delete_repository_with_cron_worker: false)
- end
-
- it 'schedules a job to delete a repository' do
- expect(DeleteContainerRepositoryWorker).to receive(:perform_async).with(user.id, repository.id)
-
- expect { delete_repository(repository) }
- .to change { repository.reload.status }.from(nil).to('delete_scheduled')
-
- expect(repository.reload).to be_delete_scheduled
- expect(response).to have_gitlab_http_status(:no_content)
- end
- end
end
end
end
diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb
index 1066c4ec9f6..5733b8114d4 100644
--- a/spec/controllers/projects/runners_controller_spec.rb
+++ b/spec/controllers/projects/runners_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::RunnersController do
+RSpec.describe Projects::RunnersController, feature_category: :runner_fleet do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:runner) { create(:ci_runner, :project, projects: [project]) }
diff --git a/spec/controllers/projects/service_ping_controller_spec.rb b/spec/controllers/projects/service_ping_controller_spec.rb
index 22fb18edc80..10d4b897564 100644
--- a/spec/controllers/projects/service_ping_controller_spec.rb
+++ b/spec/controllers/projects/service_ping_controller_spec.rb
@@ -91,11 +91,14 @@ RSpec.describe Projects::ServicePingController do
expect(response).to have_gitlab_http_status(:ok)
end
- it_behaves_like 'Snowplow event tracking' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
let(:project) { create(:project) }
- let(:category) { 'ide_edit' }
- let(:action) { 'g_edit_by_live_preview' }
let(:namespace) { project.namespace }
+ let(:category) { 'Gitlab::UsageDataCounters::EditorUniqueCounter' }
+ let(:action) { 'ide_edit' }
+ let(:property) { 'g_edit_by_live_preview' }
+ let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_ide_edit' }
+ let(:context) { [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name).to_context] }
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
end
end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index e5ae1b04a86..dcd1072612a 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -19,8 +19,10 @@ RSpec.describe Projects::Settings::CiCdController do
let_it_be(:group) { create(:group, parent: parent_group) }
let_it_be(:other_project) { create(:project, group: group) }
+ subject { get :show, params: { namespace_id: project.namespace, project_id: project } }
+
it 'renders show with 200 status code' do
- get :show, params: { namespace_id: project.namespace, project_id: project }
+ subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
@@ -32,25 +34,60 @@ RSpec.describe Projects::Settings::CiCdController do
end
it 'renders show with 404 status code' do
- get :show, params: { namespace_id: project.namespace, project_id: project }
+ subject
+
expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'with group runners' do
- let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
- let_it_be(:project_runner) { create(:ci_runner, :project, projects: [other_project]) }
- let_it_be(:shared_runner) { create(:ci_runner, :instance) }
+ context 'with assignable project runners' do
+ let(:project_runner) { create(:ci_runner, :project, projects: [other_project]) }
- it 'sets assignable project runners only' do
+ before do
group.add_maintainer(user)
+ end
- get :show, params: { namespace_id: project.namespace, project_id: project }
+ it 'sets assignable project runners' do
+ subject
expect(assigns(:assignable_runners)).to contain_exactly(project_runner)
end
end
+ context 'with project runners' do
+ let(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+
+ it 'sets project runners' do
+ subject
+
+ expect(assigns(:project_runners)).to contain_exactly(project_runner)
+ end
+ end
+
+ context 'with group runners' do
+ let_it_be(:group) { create :group }
+ let_it_be(:project) { create :project, group: group }
+ let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+
+ it 'sets group runners' do
+ subject
+
+ expect(assigns(:group_runners_count)).to be(1)
+ expect(assigns(:group_runners)).to contain_exactly(group_runner)
+ end
+ end
+
+ context 'with instance runners' do
+ let_it_be(:shared_runner) { create(:ci_runner, :instance) }
+
+ it 'sets shared runners' do
+ subject
+
+ expect(assigns(:shared_runners_count)).to be(1)
+ expect(assigns(:shared_runners)).to contain_exactly(shared_runner)
+ end
+ end
+
context 'prevents N+1 queries for tags' do
render_views
diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb
index 2b23f177a9d..2ce58a77d94 100644
--- a/spec/controllers/projects/settings/integrations_controller_spec.rb
+++ b/spec/controllers/projects/settings/integrations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Settings::IntegrationsController do
+RSpec.describe Projects::Settings::IntegrationsController, feature_category: :integrations do
include JiraIntegrationHelpers
include AfterNextHelpers
@@ -39,168 +39,174 @@ RSpec.describe Projects::Settings::IntegrationsController do
end
end
- describe '#test' do
- context 'when the integration is not testable' do
- it 'renders 404' do
- allow_any_instance_of(Integration).to receive(:testable?).and_return(false)
+ describe '#test', :clean_gitlab_redis_rate_limiting do
+ let_it_be(:integration) { create(:external_wiki_integration, project: project) }
- put :test, params: project_params
+ let(:integration_params) { { external_wiki_url: 'https://example.net/wiki' } }
- expect(response).to have_gitlab_http_status(:not_found)
+ it 'renders 404 when the integration is not testable' do
+ allow_next_found_instance_of(integration.class) do |integration|
+ allow(integration).to receive(:testable?).and_return(false)
end
- end
-
- context 'when validations fail', :clean_gitlab_redis_rate_limiting do
- let(:integration_params) { { active: 'true', url: '' } }
- it 'returns error messages in JSON response' do
- put :test, params: project_params(service: integration_params)
+ put :test, params: project_params(service: integration_params)
- expect(json_response['message']).to eq 'Validations failed.'
- expect(json_response['service_response']).to include "Url can't be blank"
- expect(response).to be_successful
- end
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response).to eq({})
end
- context 'when successful', :clean_gitlab_redis_rate_limiting do
- context 'with empty project' do
- let_it_be(:project) { create(:project) }
+ it 'returns success if test is successful' do
+ allow_next(Integrations::Test::ProjectService).to receive(:execute).and_return({ success: true })
- context 'with chat notification integration' do
- let_it_be(:teams_integration) { project.create_microsoft_teams_integration(webhook: 'http://webhook.com') }
+ put :test, params: project_params(service: integration_params)
- let(:integration) { teams_integration }
+ expect(response).to be_successful
+ expect(json_response).to eq({})
+ end
- it 'returns success' do
- allow_next(::MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
+ it 'returns extra given data if test is successful' do
+ allow_next(Integrations::Test::ProjectService).to receive(:execute)
+ .and_return({ success: true, data: { my_payload: true } })
- put :test, params: project_params
+ put :test, params: project_params(service: integration_params)
- expect(response).to be_successful
- end
- end
-
- it 'returns success' do
- stub_jira_integration_test
-
- expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original
+ expect(response).to be_successful
+ expect(json_response).to eq({ 'my_payload' => true })
+ end
- put :test, params: project_params(service: integration_params)
+ it 'returns an error response if the test is not successful' do
+ allow_next(Integrations::Test::ProjectService).to receive(:execute).and_return({ success: false })
- expect(response).to be_successful
- end
- end
+ put :test, params: project_params(service: integration_params)
- it 'returns success' do
- stub_jira_integration_test
+ expect(response).to be_successful
+ expect(json_response).to eq(
+ 'error' => true,
+ 'message' => 'Connection failed. Check your integration settings.',
+ 'service_response' => '',
+ 'test_failed' => true
+ )
+ end
- expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original
+ it 'returns extra given message if the test is not successful' do
+ allow_next(Integrations::Test::ProjectService).to receive(:execute)
+ .and_return({ success: false, result: 'Result of test' })
- put :test, params: project_params(service: integration_params)
+ put :test, params: project_params(service: integration_params)
- expect(response).to be_successful
- end
-
- context 'when service is configured for the first time' do
- let(:integration_params) do
- {
- 'active' => '1',
- 'push_events' => '1',
- 'token' => 'token',
- 'project_url' => 'https://buildkite.com/organization/pipeline'
- }
- end
+ expect(response).to be_successful
+ expect(json_response).to eq(
+ 'error' => true,
+ 'message' => 'Connection failed. Check your integration settings.',
+ 'service_response' => 'Result of test',
+ 'test_failed' => true
+ )
+ end
- before do
- allow_next(ServiceHook).to receive(:execute).and_return(true)
- end
+ it 'returns an error response if a network exception is raised' do
+ allow_next(Integrations::Test::ProjectService).to receive(:execute).and_raise(Errno::ECONNREFUSED)
- it 'persist the object' do
- do_put
+ put :test, params: project_params(service: integration_params)
- expect(response).to be_successful
- expect(json_response).to be_empty
- expect(Integrations::Buildkite.first).to be_present
- end
+ expect(response).to be_successful
+ expect(json_response).to eq(
+ 'error' => true,
+ 'message' => 'Connection failed. Check your integration settings.',
+ 'service_response' => 'Connection refused',
+ 'test_failed' => true
+ )
+ end
- it 'creates the ServiceHook object' do
- do_put
+ it 'returns error messages in JSON response if validations fail' do
+ integration_params = { active: 'true', external_wiki_url: '' }
- expect(response).to be_successful
- expect(json_response).to be_empty
- expect(Integrations::Buildkite.first.service_hook).to be_present
- end
+ put :test, params: project_params(service: integration_params)
- def do_put
- put :test, params: project_params(id: 'buildkite',
- service: integration_params)
- end
- end
+ expect(json_response['message']).to eq 'Validations failed.'
+ expect(json_response['service_response']).to eq(
+ "External wiki url can't be blank, External wiki url must be a valid URL"
+ )
+ expect(response).to be_successful
end
- context 'when unsuccessful', :clean_gitlab_redis_rate_limiting do
- it 'returns an error response when the integration test fails' do
- stub_request(:get, 'http://example.com/rest/api/2/serverInfo')
- .to_return(status: 404)
+ context 'when integration has a webhook' do
+ let_it_be(:integration) { create(:integrations_slack, project: project) }
+
+ it 'returns an error response if the webhook URL is changed to one that is blocked' do
+ integration_params = { webhook: 'http://127.0.0.1' }
put :test, params: project_params(service: integration_params)
expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
- 'message' => 'Connection failed. Check your integration settings.',
- 'service_response' => '',
- 'test_failed' => true
+ 'message' => 'Validations failed.',
+ 'service_response' => "Webhook is blocked: Requests to localhost are not allowed",
+ 'test_failed' => false
)
end
- context 'with the Slack integration' do
- let_it_be(:integration) { build(:integrations_slack) }
+ it 'ignores masked webhook param' do
+ integration_params = { active: 'true', webhook: '************' }
+ allow_next(Integrations::Test::ProjectService).to receive(:execute).and_return({ success: true })
- it 'returns an error response when the URL is blocked' do
- put :test, params: project_params(service: { webhook: 'http://127.0.0.1' })
+ expect do
+ put :test, params: project_params(service: integration_params)
+ end.not_to change { integration.reload.webhook }
- expect(response).to be_successful
- expect(json_response).to eq(
- 'error' => true,
- 'message' => 'Connection failed. Check your integration settings.',
- 'service_response' => "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed",
- 'test_failed' => true
- )
- end
+ expect(response).to be_successful
+ expect(json_response).to eq({})
+ end
- it 'returns an error response when a network exception is raised' do
- expect_next(Integrations::Slack).to receive(:test).and_raise(Errno::ECONNREFUSED)
+ it 'creates an associated web hook record if web hook integration is configured for the first time' do
+ integration_params = {
+ 'active' => '1',
+ 'issues_events' => '1',
+ 'push_events' => '0',
+ 'token' => 'my-token',
+ 'project_url' => 'https://buildkite.com/organization/pipeline'
+ }
+ allow_next(ServiceHook).to receive(:execute).and_return(true)
- put :test, params: project_params
+ expect do
+ put :test, params: project_params(id: 'buildkite', service: integration_params)
+ end.to change { Integrations::Buildkite.count }.from(0).to(1)
- expect(response).to be_successful
- expect(json_response).to eq(
- 'error' => true,
- 'message' => 'Connection failed. Check your integration settings.',
- 'service_response' => 'Connection refused',
- 'test_failed' => true
- )
- end
+ integration = Integrations::Buildkite.take
+
+ expect(response).to be_successful
+ expect(json_response).to eq({})
+ expect(integration).to have_attributes(
+ project_url: 'https://buildkite.com/organization/pipeline',
+ issues_events: true,
+ push_events: false
+ )
+ expect(integration.service_hook).to have_attributes(
+ url: 'https://webhook.buildkite.com/deliver/{webhook_token}',
+ interpolated_url: 'https://webhook.buildkite.com/deliver/my-token'
+ )
end
end
- context 'when the endpoint receives requests above the limit', :freeze_time, :clean_gitlab_redis_rate_limiting do
+ context 'when the endpoint receives requests above the rate limit', :freeze_time do
before do
allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits)
.and_return(project_testing_integration: { threshold: 1, interval: 1.minute })
end
it 'prevents making test requests' do
- stub_jira_integration_test
-
expect_next_instance_of(::Integrations::Test::ProjectService) do |service|
expect(service).to receive(:execute).and_return(http_status: 200)
end
2.times { post :test, params: project_params(service: integration_params) }
- expect(response.body).to include(_('This endpoint has been requested too many times. Try again later.'))
+ expect(json_response).to eq(
+ {
+ 'error' => true,
+ 'message' => 'This endpoint has been requested too many times. Try again later.'
+ }
+ )
expect(response).to have_gitlab_http_status(:ok)
end
end
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
index ea50ff6caa0..51ea2e5d7c6 100644
--- a/spec/controllers/projects/settings/repository_controller_spec.rb
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Settings::RepositoryController do
+RSpec.describe Projects::Settings::RepositoryController, feature_category: :source_code_management do
let(:project) { create(:project_empty_repo, :public) }
let(:user) { create(:user) }
let(:base_params) { { namespace_id: project.namespace, project_id: project } }
@@ -19,6 +19,40 @@ RSpec.describe Projects::Settings::RepositoryController do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
end
+
+ context 'when feature flag `group_protected_branches` disabled' do
+ before do
+ stub_feature_flags(group_protected_branches: false)
+ end
+
+ it 'does not assign instance variable `protected_group_branches`' do
+ get :show, params: base_params
+
+ expect(assigns).not_to include(:protected_group_branches)
+ end
+ end
+
+ context 'when feature flag `group_protected_branches` enabled' do
+ context 'when the root namespace is a user' do
+ it 'assigns empty instance variable `protected_group_branches`' do
+ get :show, params: base_params
+
+ expect(assigns[:protected_group_branches]).to eq([])
+ end
+ end
+
+ context 'when the root namespace is a group' do
+ let_it_be(:project) { create(:project_empty_repo, :public, :in_group) }
+
+ let(:protected_group_branch) { create(:protected_branch, group: project.root_namespace, project: nil) }
+
+ it 'assigns instance variable `protected_group_branches`' do
+ get :show, params: base_params
+
+ expect(assigns[:protected_group_branches]).to include(protected_group_branch)
+ end
+ end
+ end
end
describe 'PUT cleanup' do
@@ -54,7 +88,7 @@ RSpec.describe Projects::Settings::RepositoryController do
let(:good_deploy_token_params) do
{
name: 'name',
- expires_at: 1.day.from_now.to_s,
+ expires_at: 1.day.from_now.to_datetime.to_s,
username: 'deployer',
read_repository: '1',
deploy_token_type: DeployToken.deploy_token_types[:project_type]
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 446e5e38865..bc58eaa1d6f 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -702,16 +702,12 @@ RSpec.describe ProjectsController do
skip unless project.hashed_storage?(:repository)
hashed_storage_path = ::Storage::Hashed.new(project).disk_path
- original_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- project.repository.path
- end
+ original_repository_path = project.repository.relative_path
expect { update_project path: 'renamed_path' }.to change { project.reload.path }
expect(project.path).to include 'renamed_path'
- assign_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- assigns(:repository).path
- end
+ assign_repository_path = assigns(:repository).relative_path
expect(original_repository_path).to include(hashed_storage_path)
expect(assign_repository_path).to include(hashed_storage_path)
@@ -721,16 +717,12 @@ RSpec.describe ProjectsController do
skip if project.hashed_storage?(:repository)
hashed_storage_path = Storage::Hashed.new(project).disk_path
- original_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- project.repository.path
- end
+ original_repository_path = project.repository.relative_path
expect { update_project path: 'renamed_path' }.to change { project.reload.path }
expect(project.path).to include 'renamed_path'
- assign_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- assigns(:repository).path
- end
+ assign_repository_path = assigns(:repository).relative_path
expect(original_repository_path).not_to include(hashed_storage_path)
expect(assign_repository_path).to include(hashed_storage_path)
@@ -928,35 +920,6 @@ RSpec.describe ProjectsController do
with_them do
it_behaves_like 'feature update success'
end
-
- context 'for feature_access_level operations_access_level' do
- let(:feature_access_level) { :operations_access_level }
-
- include_examples 'feature update failure'
- end
-
- context 'with feature flag split_operations_visibility_permissions disabled' do
- before do
- stub_feature_flags(split_operations_visibility_permissions: false)
- end
-
- context 'for feature_access_level operations_access_level' do
- let(:feature_access_level) { :operations_access_level }
-
- include_examples 'feature update success'
- end
-
- where(:feature_access_level) do
- %i[
- environments_access_level feature_flags_access_level
- monitor_access_level
- ]
- end
-
- with_them do
- it_behaves_like 'feature update failure'
- end
- end
end
end
@@ -1334,7 +1297,7 @@ RSpec.describe ProjectsController do
text: merge_request.to_reference
}
- expect(json_response['body']).to match(/\!#{merge_request.iid} \(closed\)/)
+ expect(json_response['body']).to match(/!#{merge_request.iid} \(closed\)/)
end
end
@@ -1635,6 +1598,12 @@ RSpec.describe ProjectsController do
context 'applies correct scope when throttling', :clean_gitlab_redis_rate_limiting do
before do
stub_application_setting(project_download_export_limit: 1)
+
+ travel_to Date.current.beginning_of_day
+ end
+
+ after do
+ travel_back
end
it 'applies throttle per namespace' do
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 8775f68a5de..699052fe37a 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe RegistrationsController do
let(:session_params) { {} }
- subject { post(:create, params: user_params, session: session_params) }
+ subject(:post_create) { post(:create, params: user_params, session: session_params) }
context '`blocked_pending_approval` state' do
context 'when the `require_admin_approval_after_user_signup` setting is turned on' do
@@ -75,9 +75,9 @@ RSpec.describe RegistrationsController do
end
context 'email confirmation' do
- context 'when `send_user_confirmation_email` is true' do
+ context 'when `email_confirmation_setting` is set to `hard`' do
before do
- stub_application_setting(send_user_confirmation_email: true)
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
end
it 'does not send a confirmation email' do
@@ -122,9 +122,9 @@ RSpec.describe RegistrationsController do
end
context 'email confirmation' do
- context 'when `send_user_confirmation_email` is true' do
+ context 'when `email_confirmation_setting` is set to `hard`' do
before do
- stub_application_setting(send_user_confirmation_email: true)
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
stub_feature_flags(identity_verification: false)
end
@@ -142,18 +142,18 @@ RSpec.describe RegistrationsController do
stub_feature_flags(identity_verification: false)
end
- context 'when send_user_confirmation_email is false' do
+ context 'when `email_confirmation_setting` is set to `off`' do
it 'signs the user in' do
- stub_application_setting(send_user_confirmation_email: false)
+ stub_application_setting_enum('email_confirmation_setting', 'off')
expect { subject }.not_to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
expect(controller.current_user).not_to be_nil
end
end
- context 'when send_user_confirmation_email is true' do
+ context 'when `email_confirmation_setting` is set to `hard`' do
before do
- stub_application_setting(send_user_confirmation_email: true)
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
end
context 'when soft email confirmation is not enabled' do
@@ -167,6 +167,16 @@ RSpec.describe RegistrationsController do
expect(controller.current_user).to be_nil
end
+ it 'tracks an almost there redirect' do
+ post_create
+
+ expect_snowplow_event(
+ category: described_class.name,
+ action: 'render',
+ user: User.find_by(email: base_user_params[:email])
+ )
+ end
+
context 'when registration is triggered from an accepted invite' do
context 'when it is part from the initial invite email', :snowplow do
let_it_be(:member) { create(:project_member, :invited, invite_email: user_params.dig(:user, :email)) }
@@ -260,6 +270,16 @@ RSpec.describe RegistrationsController do
expect(response).to redirect_to(users_sign_up_welcome_path)
end
+ it 'does not track an almost there redirect' do
+ post_create
+
+ expect_no_snowplow_event(
+ category: described_class.name,
+ action: 'render',
+ user: User.find_by(email: base_user_params[:email])
+ )
+ end
+
context 'when invite email matches email used on registration' do
let(:session_params) { { invite_email: user_params.dig(:user, :email) } }
@@ -484,47 +504,56 @@ RSpec.describe RegistrationsController do
render_views
let_it_be(:new_user_params) { { new_user: base_user_params.merge({ password: "password" }) } }
- subject { post(:create, params: new_user_params) }
+ subject(:post_create) { post(:create, params: new_user_params) }
- context 'when block_weak_passwords is enabled (default)' do
- it 'renders the form with errors' do
- expect { subject }.not_to change(User, :count)
+ it 'renders the form with errors' do
+ expect { post_create }.not_to change(User, :count)
- expect(controller.current_user).to be_nil
- expect(response).to render_template(:new)
- expect(response.body).to include(_('Password must not contain commonly used combinations of words and letters'))
- end
+ expect(controller.current_user).to be_nil
+ expect(response).to render_template(:new)
+ expect(response.body).to include(_('Password must not contain commonly used combinations of words and letters'))
+ end
- it 'tracks the error' do
- subject
- expect_snowplow_event(
- category: 'Gitlab::Tracking::Helpers::WeakPasswordErrorEvent',
- action: 'track_weak_password_error',
- controller: 'RegistrationsController',
- method: 'create'
- )
- end
+ it 'tracks a weak password error' do
+ post_create
+
+ expect_snowplow_event(
+ category: 'Gitlab::Tracking::Helpers::WeakPasswordErrorEvent',
+ action: 'track_weak_password_error',
+ controller: 'RegistrationsController',
+ method: 'create'
+ )
end
- context 'when block_weak_passwords is disabled' do
- before do
- stub_feature_flags(block_weak_passwords: false)
- end
+ it 'does not track failed form submission' do
+ post_create
- it 'permits weak passwords' do
- expect { subject }.to change(User, :count).by(1)
- end
+ expect_no_snowplow_event(
+ category: described_class.name,
+ action: 'successfully_submitted_form'
+ )
end
end
context 'when the password is not weak' do
it 'does not track a weak password error' do
- subject
+ post_create
+
expect_no_snowplow_event(
category: 'Gitlab::Tracking::Helpers::WeakPasswordErrorEvent',
action: 'track_weak_password_error'
)
end
+
+ it 'tracks successful form submission' do
+ post_create
+
+ expect_snowplow_event(
+ category: described_class.name,
+ action: 'successfully_submitted_form',
+ user: User.find_by(email: base_user_params[:email])
+ )
+ end
end
context 'with preferred language' do
@@ -552,6 +581,39 @@ RSpec.describe RegistrationsController do
end
end
end
+
+ context 'when the first or last name is not "present?"' do
+ using RSpec::Parameterized::TableSyntax
+
+ render_views
+
+ shared_examples 'a user without present first name or last name' do
+ it 'renders the form with errors' do
+ subject
+ expect(controller.current_user).to be_nil
+ expect(response).to render_template(:new)
+ expect(response.body).to include(_('name cannot be blank')) # include 'First name' or 'Last name' or both
+ end
+ end
+
+ where(:first_name, :last_name) do
+ nil | 'last'
+ '' | 'last'
+ ' ' | 'last'
+ 'first' | nil
+ 'first' | ''
+ 'first' | ' '
+ '' | ''
+ end
+
+ with_them do
+ before do
+ base_user_params.merge!({ first_name: first_name, last_name: last_name })
+ end
+
+ it_behaves_like 'a user without present first name or last name'
+ end
+ end
end
describe '#destroy' do
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 21df53fb074..37fc5a033ba 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -421,6 +421,12 @@ RSpec.describe SearchController do
expect(json_response.count).to eq(1)
expect(json_response.first['label']).to match(/User settings/)
end
+
+ it 'makes a call to search_autocomplete_opts' do
+ expect(controller).to receive(:search_autocomplete_opts).once
+
+ get :autocomplete, params: { term: 'setting', filter: 'generic' }
+ end
end
describe '#append_info_to_payload' do
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 69282f951f9..78b3cc63b08 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -477,6 +477,22 @@ RSpec.describe SessionsController do
expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { AuthenticationEvent.count }.by(1)
expect(AuthenticationEvent.last.provider).to eq("two-factor")
end
+
+ context 'when rendering devise two factor' do
+ render_views
+
+ before do
+ Gon.clear
+ end
+
+ it "adds gon variables" do
+ authenticate_2fa(login: user.username, password: user.password)
+
+ expect(response).to render_template('devise/sessions/two_factor')
+ expect(Gon.all_variables).not_to be_empty
+ expect(response.body).to match('gon.api_version')
+ end
+ end
end
context 'when using two-factor authentication via U2F device' do
diff --git a/spec/db/development/create_work_item_hierarchy_restrictions_spec.rb b/spec/db/development/create_work_item_hierarchy_restrictions_spec.rb
new file mode 100644
index 00000000000..0e60ecd08c0
--- /dev/null
+++ b/spec/db/development/create_work_item_hierarchy_restrictions_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create work item hierarchy restrictions in development', feature_category: :portfolio_management do
+ subject { load Rails.root.join('db/fixtures/development/50_create_work_item_hierarchy_restrictions.rb') }
+
+ it_behaves_like 'work item hierarchy restrictions importer'
+end
diff --git a/spec/db/docs_spec.rb b/spec/db/docs_spec.rb
index ad3705c3dbe..6cfff725988 100644
--- a/spec/db/docs_spec.rb
+++ b/spec/db/docs_spec.rb
@@ -2,108 +2,95 @@
require 'spec_helper'
-RSpec.describe 'Database Documentation' do
- context 'for each table' do
- # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/366834
- let(:database_base_models) { Gitlab::Database.database_base_models.select { |k, _| k != 'geo' } }
-
- let(:all_tables) do
- database_base_models.flat_map { |_, m| m.connection.tables }.sort.uniq
- end
-
- let(:metadata_required_fields) do
- %i(
- feature_categories
- table_name
- )
- end
+RSpec.shared_examples 'validate dictionary' do |objects, directory_path, required_fields|
+ context 'for each object' do
+ let(:directory_path) { directory_path }
let(:metadata_allowed_fields) do
- metadata_required_fields + %i(
+ required_fields + %i[
classes
description
introduced_by_url
milestone
- )
+ gitlab_schema
+ ]
end
let(:metadata) do
- all_tables.each_with_object({}) do |table_name, hash|
- next unless File.exist?(table_metadata_file_path(table_name))
+ objects.each_with_object({}) do |object_name, hash|
+ next unless File.exist?(object_metadata_file_path(object_name))
- hash[table_name] ||= load_table_metadata(table_name)
+ hash[object_name] ||= load_object_metadata(required_fields, object_name)
end
end
- let(:tables_without_metadata) do
- all_tables.reject { |t| metadata.has_key?(t) }
+ let(:objects_without_metadata) do
+ objects.reject { |t| metadata.has_key?(t) }
end
- let(:tables_without_valid_metadata) do
+ let(:objects_without_valid_metadata) do
metadata.select { |_, t| t.has_key?(:error) }.keys
end
- let(:tables_with_disallowed_fields) do
+ let(:objects_with_disallowed_fields) do
metadata.select { |_, t| t.has_key?(:disallowed_fields) }.keys
end
- let(:tables_with_missing_required_fields) do
+ let(:objects_with_missing_required_fields) do
metadata.select { |_, t| t.has_key?(:missing_required_fields) }.keys
end
it 'has a metadata file' do
- expect(tables_without_metadata).to be_empty, multiline_error(
+ expect(objects_without_metadata).to be_empty, multiline_error(
'Missing metadata files',
- tables_without_metadata.map { |t| " #{table_metadata_file(t)}" }
+ objects_without_metadata.map { |t| " #{object_metadata_file(t)}" }
)
end
it 'has a valid metadata file' do
- expect(tables_without_valid_metadata).to be_empty, table_metadata_errors(
+ expect(objects_without_valid_metadata).to be_empty, object_metadata_errors(
'Table metadata files with errors',
:error,
- tables_without_valid_metadata
+ objects_without_valid_metadata
)
end
it 'has a valid metadata file with allowed fields' do
- expect(tables_with_disallowed_fields).to be_empty, table_metadata_errors(
+ expect(objects_with_disallowed_fields).to be_empty, object_metadata_errors(
'Table metadata files with disallowed fields',
:disallowed_fields,
- tables_with_disallowed_fields
+ objects_with_disallowed_fields
)
end
it 'has a valid metadata file without missing fields' do
- expect(tables_with_missing_required_fields).to be_empty, table_metadata_errors(
+ expect(objects_with_missing_required_fields).to be_empty, object_metadata_errors(
'Table metadata files with missing fields',
:missing_required_fields,
- tables_with_missing_required_fields
+ objects_with_missing_required_fields
)
end
end
private
- def table_metadata_file(table_name)
- File.join('db', 'docs', "#{table_name}.yml")
+ def object_metadata_file(object_name)
+ File.join(directory_path, "#{object_name}.yml")
end
- def table_metadata_file_path(table_name)
- Rails.root.join(table_metadata_file(table_name))
+ def object_metadata_file_path(object_name)
+ Rails.root.join(object_metadata_file(object_name))
end
- def load_table_metadata(table_name)
+ def load_object_metadata(required_fields, object_name)
result = {}
begin
- result[:metadata] = YAML.safe_load(File.read(table_metadata_file_path(table_name))).deep_symbolize_keys
+ result[:metadata] = YAML.safe_load(File.read(object_metadata_file_path(object_name))).deep_symbolize_keys
disallowed_fields = (result[:metadata].keys - metadata_allowed_fields)
- unless disallowed_fields.empty?
- result[:disallowed_fields] = "fields not allowed: #{disallowed_fields.join(', ')}"
- end
+ result[:disallowed_fields] = "fields not allowed: #{disallowed_fields.join(', ')}" unless disallowed_fields.empty?
- missing_required_fields = (metadata_required_fields - result[:metadata].reject { |_, v| v.blank? }.keys)
+ missing_required_fields = (required_fields - result[:metadata].reject { |_, v| v.blank? }.keys)
unless missing_required_fields.empty?
result[:missing_required_fields] = "missing required fields: #{missing_required_fields.join(', ')}"
end
@@ -113,11 +100,12 @@ RSpec.describe 'Database Documentation' do
result
end
- def table_metadata_errors(title, field, tables)
- lines = tables.map do |table_name|
+ # rubocop:disable Naming/HeredocDelimiterNaming
+ def object_metadata_errors(title, field, objects)
+ lines = objects.map do |object_name|
<<~EOM
- #{table_metadata_file(table_name)}
- #{metadata[table_name][field]}
+ #{object_metadata_file(object_name)}
+ #{metadata[object_name][field]}
EOM
end
@@ -131,4 +119,23 @@ RSpec.describe 'Database Documentation' do
#{lines.join("\n")}
EOM
end
+ # rubocop:enable Naming/HeredocDelimiterNaming
+end
+
+RSpec.describe 'Views documentation', feature_category: :database do
+ database_base_models = Gitlab::Database.database_base_models.select { |k, _| k != 'geo' }
+ 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]
+
+ include_examples 'validate dictionary', views, directory_path, required_fields
+end
+
+RSpec.describe 'Tables documentation', feature_category: :database do
+ database_base_models = Gitlab::Database.database_base_models.select { |k, _| k != 'geo' }
+ 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]
+
+ include_examples 'validate dictionary', tables, directory_path, required_fields
end
diff --git a/spec/db/migration_spec.rb b/spec/db/migration_spec.rb
index 7751bfd989d..b5f6192233f 100644
--- a/spec/db/migration_spec.rb
+++ b/spec/db/migration_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe 'Migrations Validation' 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],
2022_01_26_21_06_58.. => 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/db/production/create_work_item_hierarchy_restrictions_spec.rb b/spec/db/production/create_work_item_hierarchy_restrictions_spec.rb
new file mode 100644
index 00000000000..5b47d88d71a
--- /dev/null
+++ b/spec/db/production/create_work_item_hierarchy_restrictions_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create work item hierarchy restrictions in production', feature_category: :portfolio_management do
+ subject { load Rails.root.join('db/fixtures/production/020_create_work_item_hierarchy_restrictions.rb') }
+
+ it_behaves_like 'work item hierarchy restrictions importer'
+end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index ad49a763361..9e23cca7c3f 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -6,12 +6,11 @@ require Rails.root.join('ee', 'spec', 'db', 'schema_support') if Gitlab.ee?
RSpec.describe 'Database schema' do
prepend_mod_with('DB::SchemaSupport')
- let(:connection) { ActiveRecord::Base.connection }
let(:tables) { connection.tables }
let(:columns_name_with_jsonb) { retrieve_columns_name_with_jsonb }
IGNORED_INDEXES_ON_FKS = {
- issues: %w[work_item_type_id]
+ slack_integrations_scopes: %w[slack_api_scope_id]
}.with_indifferent_access.freeze
TABLE_PARTITIONS = %w[ci_builds_metadata].freeze
@@ -33,15 +32,27 @@ RSpec.describe 'Database schema' do
boards: %w[milestone_id iteration_id],
chat_names: %w[chat_id team_id user_id],
chat_teams: %w[team_id],
+ ci_build_needs: %w[partition_id],
+ ci_build_pending_states: %w[partition_id],
+ ci_build_report_results: %w[partition_id],
+ ci_build_trace_chunks: %w[partition_id],
+ ci_build_trace_metadata: %w[partition_id],
ci_builds: %w[erased_by_id trigger_request_id partition_id],
+ ci_builds_runner_session: %w[partition_id],
p_ci_builds_metadata: %w[partition_id],
ci_job_artifacts: %w[partition_id],
+ ci_job_variables: %w[partition_id],
ci_namespace_monthly_usages: %w[namespace_id],
+ ci_pending_builds: %w[partition_id],
ci_pipeline_variables: %w[partition_id],
ci_pipelines: %w[partition_id],
+ ci_resources: %w[partition_id],
ci_runner_projects: %w[runner_id],
+ ci_running_builds: %w[partition_id],
+ ci_sources_pipelines: %w[partition_id source_partition_id],
ci_stages: %w[partition_id],
ci_trigger_requests: %w[commit_id],
+ ci_unit_test_failures: %w[partition_id],
cluster_providers_aws: %w[security_group_id vpc_id access_key_id],
cluster_providers_gcp: %w[gcp_project_id operation_id],
compliance_management_frameworks: %w[group_id],
@@ -109,56 +120,62 @@ RSpec.describe 'Database schema' do
}.with_indifferent_access.freeze
context 'for table' do
- (ActiveRecord::Base.connection.tables - TABLE_PARTITIONS).sort.each do |table|
- describe table do
- let(:indexes) { connection.indexes(table) }
- let(:columns) { connection.columns(table) }
- let(:foreign_keys) { connection.foreign_keys(table) }
- let(:loose_foreign_keys) { Gitlab::Database::LooseForeignKeys.definitions.group_by(&:from_table).fetch(table, []) }
- let(:all_foreign_keys) { foreign_keys + loose_foreign_keys }
- # take the first column in case we're using a composite primary key
- let(:primary_key_column) { Array(connection.primary_key(table)).first }
-
- context 'all foreign keys' do
- # for index to be effective, the FK constraint has to be at first place
- it 'are indexed' do
- first_indexed_column = indexes.filter_map do |index|
- columns = index.columns
-
- # In cases of complex composite indexes, a string is returned eg:
- # "lower((extern_uid)::text), group_id"
- columns = columns.split(',') if columns.is_a?(String)
- column = columns.first.chomp
-
- # A partial index is not suitable for a foreign key column, unless
- # the only condition is for the presence of the foreign key itself
- column if index.where.nil? || index.where == "(#{column} IS NOT NULL)"
+ Gitlab::Database::EachDatabase.each_database_connection do |connection, _|
+ schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
+ (connection.tables - TABLE_PARTITIONS).sort.each do |table|
+ table_schema = Gitlab::Database::GitlabSchema.table_schema(table)
+ next unless schemas_for_connection.include?(table_schema)
+
+ describe table do
+ let(:indexes) { connection.indexes(table) }
+ let(:columns) { connection.columns(table) }
+ let(:foreign_keys) { connection.foreign_keys(table) }
+ let(:loose_foreign_keys) { Gitlab::Database::LooseForeignKeys.definitions.group_by(&:from_table).fetch(table, []) }
+ let(:all_foreign_keys) { foreign_keys + loose_foreign_keys }
+ # take the first column in case we're using a composite primary key
+ let(:primary_key_column) { Array(connection.primary_key(table)).first }
+
+ context 'all foreign keys' do
+ # for index to be effective, the FK constraint has to be at first place
+ it 'are indexed' do
+ first_indexed_column = indexes.filter_map do |index|
+ columns = index.columns
+
+ # In cases of complex composite indexes, a string is returned eg:
+ # "lower((extern_uid)::text), group_id"
+ columns = columns.split(',') if columns.is_a?(String)
+ column = columns.first.chomp
+
+ # A partial index is not suitable for a foreign key column, unless
+ # the only condition is for the presence of the foreign key itself
+ column if index.where.nil? || index.where == "(#{column} IS NOT NULL)"
+ end
+ foreign_keys_columns = all_foreign_keys.map(&:column)
+ required_indexed_columns = foreign_keys_columns - ignored_index_columns(table)
+
+ # Add the primary key column to the list of indexed columns because
+ # postgres and mysql both automatically create an index on the primary
+ # key. Also, the rails connection.indexes() method does not return
+ # automatically generated indexes (like the primary key index).
+ first_indexed_column.push(primary_key_column)
+
+ expect(first_indexed_column.uniq).to include(*required_indexed_columns)
end
- foreign_keys_columns = all_foreign_keys.map(&:column)
- required_indexed_columns = foreign_keys_columns - ignored_index_columns(table)
-
- # Add the primary key column to the list of indexed columns because
- # postgres and mysql both automatically create an index on the primary
- # key. Also, the rails connection.indexes() method does not return
- # automatically generated indexes (like the primary key index).
- first_indexed_column.push(primary_key_column)
-
- expect(first_indexed_column.uniq).to include(*required_indexed_columns)
end
- end
- context 'columns ending with _id' do
- let(:column_names) { columns.map(&:name) }
- let(:column_names_with_id) { column_names.select { |column_name| column_name.ends_with?('_id') } }
- let(:foreign_keys_columns) { all_foreign_keys.map(&:column).uniq } # we can have FK and loose FK present at the same time
- let(:ignored_columns) { ignored_fk_columns(table) }
+ context 'columns ending with _id' do
+ let(:column_names) { columns.map(&:name) }
+ let(:column_names_with_id) { column_names.select { |column_name| column_name.ends_with?('_id') } }
+ let(:foreign_keys_columns) { all_foreign_keys.map(&:column).uniq } # we can have FK and loose FK present at the same time
+ let(:ignored_columns) { ignored_fk_columns(table) }
- it 'do have the foreign keys' do
- expect(column_names_with_id - ignored_columns).to match_array(foreign_keys_columns)
- end
+ it 'do have the foreign keys' do
+ expect(column_names_with_id - ignored_columns).to match_array(foreign_keys_columns)
+ end
- it 'and having foreign key are not in the ignore list' do
- expect(ignored_columns).to match_array(ignored_columns - foreign_keys)
+ it 'and having foreign key are not in the ignore list' do
+ expect(ignored_columns).to match_array(ignored_columns - foreign_keys)
+ end
end
end
end
@@ -225,7 +242,8 @@ RSpec.describe 'Database schema' do
"Packages::Composer::Metadatum" => %w[composer_json],
"RawUsageData" => %w[payload], # Usage data payload changes often, we cannot use one schema
"Releases::Evidence" => %w[summary],
- "Vulnerabilities::Finding::Evidence" => %w[data] # Validation work in progress
+ "Vulnerabilities::Finding::Evidence" => %w[data], # Validation work in progress
+ "EE::Gitlab::BackgroundMigration::FixSecurityScanStatuses::SecurityScan" => %w[info] # This is a migration model
}.freeze
# We are skipping GEO models for now as it adds up complexity
@@ -275,13 +293,16 @@ RSpec.describe 'Database schema' do
context 'primary keys' do
it 'expects every table to have a primary key defined' do
- connection = ActiveRecord::Base.connection
+ Gitlab::Database::EachDatabase.each_database_connection do |connection, _|
+ schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
- problematic_tables = connection.tables.select do |table|
- !connection.primary_key(table).present?
- end.map(&:to_sym)
+ problematic_tables = connection.tables.select do |table|
+ table_schema = Gitlab::Database::GitlabSchema.table_schema(table)
+ schemas_for_connection.include?(table_schema) && !connection.primary_key(table).present?
+ end.map(&:to_sym)
- expect(problematic_tables).to be_empty
+ expect(problematic_tables).to be_empty
+ end
end
end
diff --git a/spec/factories/achievements/achievements.rb b/spec/factories/achievements/achievements.rb
new file mode 100644
index 00000000000..080a0376999
--- /dev/null
+++ b/spec/factories/achievements/achievements.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :achievement, class: 'Achievements::Achievement' do
+ namespace
+
+ name { generate(:name) }
+ end
+end
diff --git a/spec/factories/bulk_import.rb b/spec/factories/bulk_import.rb
index 748afc0c67c..54d05264269 100644
--- a/spec/factories/bulk_import.rb
+++ b/spec/factories/bulk_import.rb
@@ -5,6 +5,7 @@ FactoryBot.define do
user
source_type { :gitlab }
source_version { BulkImport.min_gl_version_for_project_migration.to_s }
+ source_enterprise { false }
trait :created do
status { 0 }
diff --git a/spec/factories/bulk_import/trackers.rb b/spec/factories/bulk_import/trackers.rb
index 22e0aa096fc..3e69ab26801 100644
--- a/spec/factories/bulk_import/trackers.rb
+++ b/spec/factories/bulk_import/trackers.rb
@@ -7,23 +7,22 @@ FactoryBot.define do
stage { 0 }
has_next_page { false }
sequence(:pipeline_name) { |n| "pipeline_name_#{n}" }
+ sequence(:jid) { |n| "bulk_import_entity_#{n}" }
trait :started do
status { 1 }
-
- sequence(:jid) { |n| "bulk_import_entity_#{n}" }
end
trait :finished do
status { 2 }
-
- sequence(:jid) { |n| "bulk_import_entity_#{n}" }
end
trait :failed do
status { -1 }
+ end
- sequence(:jid) { |n| "bulk_import_entity_#{n}" }
+ trait :skipped do
+ status { -2 }
end
end
end
diff --git a/spec/factories/ci/build_runner_sessions.rb b/spec/factories/ci/build_runner_sessions.rb
new file mode 100644
index 00000000000..f78eaa6a5f1
--- /dev/null
+++ b/spec/factories/ci/build_runner_sessions.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_build_runner_session, class: 'Ci::BuildRunnerSession' do
+ build factory: :ci_build
+ url { 'https://gitlab.example.com' }
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index b88d6b5fda4..15a88955e05 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -7,6 +7,7 @@ FactoryBot.define do
created_at { 'Di 29. Okt 09:50:00 CET 2013' }
scheduling_type { 'stage' }
pending
+ partition_id { pipeline.partition_id }
options do
{
@@ -24,6 +25,8 @@ FactoryBot.define do
project { pipeline.project }
+ ref { pipeline.ref }
+
trait :with_token do
transient do
generate_token { true }
@@ -545,9 +548,12 @@ FactoryBot.define do
options do
{
image: { name: 'image:1.0', entrypoint: '/bin/sh' },
- services: ['postgres', { name: 'docker:stable-dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }, { name: 'mysql:latest', variables: { MYSQL_ROOT_PASSWORD: 'root123.' } }],
+ services: ['postgres',
+ { name: 'docker:stable-dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' },
+ { name: 'mysql:latest', variables: { MYSQL_ROOT_PASSWORD: 'root123.' } }],
script: %w(echo),
after_script: %w(ls date),
+ hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] },
artifacts: {
name: 'artifacts_file',
untracked: false,
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 891628a0fc2..eef5c593e0f 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -8,7 +8,7 @@ FactoryBot.define do
sha { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
status { 'pending' }
add_attribute(:protected) { false }
- partition_id { 100 }
+ partition_id { Ci::Pipeline.current_partition_value }
project
@@ -54,7 +54,6 @@ FactoryBot.define do
end
factory :ci_pipeline do
- partition_id { 100 }
transient { ci_ref_presence { true } }
before(:create) do |pipeline, evaluator|
@@ -84,6 +83,7 @@ FactoryBot.define do
end
trait :running do
+ started_at { Time.current }
status { :running }
end
diff --git a/spec/factories/ci/resource.rb b/spec/factories/ci/resource.rb
index dec26013a25..946cf9c17a7 100644
--- a/spec/factories/ci/resource.rb
+++ b/spec/factories/ci/resource.rb
@@ -6,6 +6,7 @@ FactoryBot.define do
trait(:retained) do
processable factory: :ci_build
+ partition_id { processable.partition_id }
end
end
end
diff --git a/spec/factories/ci/sources/pipelines.rb b/spec/factories/ci/sources/pipelines.rb
index 93d35097eac..bfe487eb6bb 100644
--- a/spec/factories/ci/sources/pipelines.rb
+++ b/spec/factories/ci/sources/pipelines.rb
@@ -4,8 +4,8 @@ FactoryBot.define do
factory :ci_sources_pipeline, class: 'Ci::Sources::Pipeline' do
after(:build) do |source|
source.project ||= source.pipeline.project
- source.source_pipeline ||= source.source_job.pipeline
- source.source_project ||= source.source_pipeline.project
+ source.source_pipeline ||= source.source_job&.pipeline
+ source.source_project ||= source.source_pipeline&.project
end
source_job factory: :ci_build
diff --git a/spec/factories/ci/unit_test_failure.rb b/spec/factories/ci/unit_test_failures.rb
index 07cd3419754..07cd3419754 100644
--- a/spec/factories/ci/unit_test_failure.rb
+++ b/spec/factories/ci/unit_test_failures.rb
diff --git a/spec/factories/ci/unit_test.rb b/spec/factories/ci/unit_tests.rb
index 480724f260a..480724f260a 100644
--- a/spec/factories/ci/unit_test.rb
+++ b/spec/factories/ci/unit_tests.rb
diff --git a/spec/factories/clusters/agents/group_authorizations.rb b/spec/factories/clusters/agents/group_authorizations.rb
index 6ea3668dc66..abe25794234 100644
--- a/spec/factories/clusters/agents/group_authorizations.rb
+++ b/spec/factories/clusters/agents/group_authorizations.rb
@@ -5,6 +5,14 @@ FactoryBot.define do
association :agent, factory: :cluster_agent
group
- config { { default_namespace: 'production' } }
+ transient do
+ environments { nil }
+ end
+
+ config do
+ { default_namespace: 'production' }.tap do |c|
+ c[:environments] = environments if environments
+ end
+ end
end
end
diff --git a/spec/factories/clusters/agents/project_authorizations.rb b/spec/factories/clusters/agents/project_authorizations.rb
index 176ecc3b517..eecbfe95bfc 100644
--- a/spec/factories/clusters/agents/project_authorizations.rb
+++ b/spec/factories/clusters/agents/project_authorizations.rb
@@ -5,6 +5,14 @@ FactoryBot.define do
association :agent, factory: :cluster_agent
project
- config { { default_namespace: 'production' } }
+ transient do
+ environments { nil }
+ end
+
+ config do
+ { default_namespace: 'production' }.tap do |c|
+ c[:environments] = environments if environments
+ end
+ end
end
end
diff --git a/spec/factories/dependency_proxy.rb b/spec/factories/dependency_proxy.rb
index 33356a701df..43cc923a4c5 100644
--- a/spec/factories/dependency_proxy.rb
+++ b/spec/factories/dependency_proxy.rb
@@ -23,14 +23,21 @@ FactoryBot.define do
factory :dependency_proxy_manifest, class: 'DependencyProxy::Manifest' do
group
size { 1234 }
- file { fixture_file_upload('spec/fixtures/dependency_proxy/manifest') }
digest { 'sha256:d0710affa17fad5f466a70159cc458227bd25d4afb39514ef662ead3e6c99515' }
sequence(:file_name) { |n| "alpine:latest#{n}.json" }
content_type { 'application/vnd.docker.distribution.manifest.v2+json' }
status { :default }
+ after(:build) do |manifest, _evaluator|
+ manifest.file = fixture_file_upload('spec/fixtures/dependency_proxy/manifest')
+ end
+
trait :pending_destruction do
status { :pending_destruction }
end
+
+ trait :remote_store do
+ file_store { DependencyProxy::FileUploader::Store::REMOTE }
+ end
end
end
diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb
index a2116b738fd..45e92869e22 100644
--- a/spec/factories/deploy_tokens.rb
+++ b/spec/factories/deploy_tokens.rb
@@ -10,7 +10,7 @@ FactoryBot.define do
read_package_registry { false }
write_package_registry { false }
revoked { false }
- expires_at { 5.days.from_now }
+ expires_at { 5.days.from_now.to_datetime }
deploy_token_type { DeployToken.deploy_token_types[:project_type] }
trait :revoked do
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index a4f06a48621..0f564afe822 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -64,6 +64,11 @@ FactoryBot.define do
target_type { 'WorkItem' }
end
+ trait :for_merge_request do
+ target { association(:merge_request) }
+ target_type { 'MergeRequest' }
+ end
+
factory :design_event, traits: [:has_design] do
action { :created }
target { design }
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 6f9cf0ef895..f4d47b9ff8c 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -118,5 +118,9 @@ FactoryBot.define do
create(:crm_settings, group: group, enabled: true)
end
end
+
+ trait :with_root_storage_statistics do
+ association :root_storage_statistics, factory: :namespace_root_storage_statistics
+ end
end
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 88522737e06..70a4a3ec822 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -4,6 +4,7 @@ FactoryBot.define do
factory :issue, traits: [:has_internal_id] do
title { generate(:title) }
project
+ namespace { project.project_namespace }
author { project.creator }
updated_by { author }
relative_position { RelativePositioning::START_POSITION }
@@ -70,6 +71,16 @@ FactoryBot.define do
association :work_item_type, :default, :task
end
+ trait :objective do
+ issue_type { :objective }
+ association :work_item_type, :default, :objective
+ end
+
+ trait :key_result do
+ issue_type { :key_result }
+ association :work_item_type, :default, :key_result
+ end
+
factory :incident do
issue_type { :incident }
association :work_item_type, :default, :incident
diff --git a/spec/factories/ml/candidate_metadata.rb b/spec/factories/ml/candidate_metadata.rb
new file mode 100644
index 00000000000..e941ae4deb8
--- /dev/null
+++ b/spec/factories/ml/candidate_metadata.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ml_candidate_metadata, class: '::Ml::CandidateMetadata' do
+ association :candidate, factory: :ml_candidates
+
+ sequence(:name) { |n| "metadata_#{n}" }
+ sequence(:value) { |n| "value#{n}" }
+ end
+end
diff --git a/spec/factories/ml/candidates.rb b/spec/factories/ml/candidates.rb
index 4fbcdc46103..2daed36d777 100644
--- a/spec/factories/ml/candidates.rb
+++ b/spec/factories/ml/candidates.rb
@@ -10,5 +10,11 @@ FactoryBot.define do
candidate.params = FactoryBot.create_list(:ml_candidate_params, 2, candidate: candidate )
end
end
+
+ trait :with_metadata do
+ after(:create) do |candidate|
+ candidate.metadata = FactoryBot.create_list(:ml_candidate_metadata, 2, candidate: candidate )
+ end
+ end
end
end
diff --git a/spec/factories/ml/experiment_metadata.rb b/spec/factories/ml/experiment_metadata.rb
new file mode 100644
index 00000000000..d3ece9630a4
--- /dev/null
+++ b/spec/factories/ml/experiment_metadata.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ml_experiment_metadata, class: '::Ml::ExperimentMetadata' do
+ association :experiment, factory: :ml_experiments
+
+ sequence(:name) { |n| "metadata_#{n}" }
+ sequence(:value) { |n| "value#{n}" }
+ end
+end
diff --git a/spec/factories/ml/experiments.rb b/spec/factories/ml/experiments.rb
index e4f5a0da6cf..0acb4c5c5fc 100644
--- a/spec/factories/ml/experiments.rb
+++ b/spec/factories/ml/experiments.rb
@@ -4,6 +4,12 @@ FactoryBot.define do
sequence(:name) { |n| "experiment#{n}" }
project
- user
+ user { project&.creator }
+
+ trait :with_metadata do
+ after(:create) do |e|
+ e.metadata = FactoryBot.create_list(:ml_experiment_metadata, 2, experiment: e) # rubocop:disable StrategyInCallback
+ end
+ end
end
end
diff --git a/spec/factories/packages/rpm/rpm_repository_files.rb b/spec/factories/packages/rpm/rpm_repository_files.rb
index 00755f49d98..7b86c593627 100644
--- a/spec/factories/packages/rpm/rpm_repository_files.rb
+++ b/spec/factories/packages/rpm/rpm_repository_files.rb
@@ -34,5 +34,9 @@ FactoryBot.define do
trait :pending_destruction do
status { :pending_destruction }
end
+
+ trait :filelists do
+ file_name { 'filelists.xml' }
+ end
end
end
diff --git a/spec/factories/project_export_jobs.rb b/spec/factories/project_export_jobs.rb
index b2666555ea8..bf8cfd863ec 100644
--- a/spec/factories/project_export_jobs.rb
+++ b/spec/factories/project_export_jobs.rb
@@ -4,5 +4,21 @@ FactoryBot.define do
factory :project_export_job do
project
jid { SecureRandom.hex(8) }
+
+ trait :queued do
+ status { ProjectExportJob::STATUS[:queued] }
+ end
+
+ trait :started do
+ status { ProjectExportJob::STATUS[:started] }
+ end
+
+ trait :finished do
+ status { ProjectExportJob::STATUS[:finished] }
+ end
+
+ trait :failed do
+ status { ProjectExportJob::STATUS[:failed] }
+ end
end
end
diff --git a/spec/factories/projects/ci_feature_usages.rb b/spec/factories/projects/ci_feature_usages.rb
index 1ab1d82ef4b..48e5331afcc 100644
--- a/spec/factories/projects/ci_feature_usages.rb
+++ b/spec/factories/projects/ci_feature_usages.rb
@@ -4,6 +4,7 @@ FactoryBot.define do
factory :project_ci_feature_usage, class: 'Projects::CiFeatureUsage' do
project factory: :project
feature { :code_coverage } # rubocop: disable RSpec/EmptyExampleGroup
+
default_branch { false }
end
end
diff --git a/spec/factories/projects/import_export/relation_export_upload.rb b/spec/factories/projects/import_export/relation_export_upload.rb
index eaa57d6ee59..4bd6a586720 100644
--- a/spec/factories/projects/import_export/relation_export_upload.rb
+++ b/spec/factories/projects/import_export/relation_export_upload.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :project_relation_export_upload, class: 'Projects::ImportExport::RelationExportUpload' do
+ factory :relation_export_upload, class: 'Projects::ImportExport::RelationExportUpload' do
relation_export factory: :project_relation_export
export_file { fixture_file_upload("spec/fixtures/gitlab/import_export/labels.tar.gz") }
end
diff --git a/spec/factories/resource_milestone_event.rb b/spec/factories/resource_milestone_events.rb
index a3944e013da..a3944e013da 100644
--- a/spec/factories/resource_milestone_event.rb
+++ b/spec/factories/resource_milestone_events.rb
diff --git a/spec/factories/resource_state_event.rb b/spec/factories/resource_state_events.rb
index 926c6dd8cbc..926c6dd8cbc 100644
--- a/spec/factories/resource_state_event.rb
+++ b/spec/factories/resource_state_events.rb
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 97a1265c46a..760367539fc 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -41,6 +41,10 @@ FactoryBot.define do
action { Todo::UNMERGEABLE }
end
+ trait :member_access_requested do
+ action { Todo::MEMBER_ACCESS_REQUESTED }
+ end
+
trait :pending do
state { :pending }
end
diff --git a/spec/factories/work_items.rb b/spec/factories/work_items.rb
index 205b071a5d4..cff246d4071 100644
--- a/spec/factories/work_items.rb
+++ b/spec/factories/work_items.rb
@@ -27,5 +27,15 @@ FactoryBot.define do
trait :last_edited_by_user do
association :last_edited_by, factory: :user
end
+
+ trait :objective do
+ issue_type { :objective }
+ association :work_item_type, :default, :objective
+ end
+
+ trait :key_result do
+ issue_type { :key_result }
+ association :work_item_type, :default, :key_result
+ end
end
end
diff --git a/spec/factories/work_items/hierarchy_restrictions.rb b/spec/factories/work_items/hierarchy_restrictions.rb
new file mode 100644
index 00000000000..09a10b633ba
--- /dev/null
+++ b/spec/factories/work_items/hierarchy_restrictions.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :hierarchy_restriction, class: 'WorkItems::HierarchyRestriction' do
+ parent_type { association :work_item_type, :default }
+ child_type { association :work_item_type, :default }
+ end
+end
diff --git a/spec/factories/work_items/work_item_types.rb b/spec/factories/work_items/work_item_types.rb
index 1b6137503d3..d36cb6260c6 100644
--- a/spec/factories/work_items/work_item_types.rb
+++ b/spec/factories/work_items/work_item_types.rb
@@ -13,7 +13,7 @@ FactoryBot.define do
# Expect base_types to exist on the DB
if type_base_attributes.slice(:namespace, :namespace_id).compact.empty?
- WorkItems::Type.find_or_initialize_by(type_base_attributes).tap { |type| type.assign_attributes(attributes) }
+ WorkItems::Type.find_or_initialize_by(type_base_attributes)
else
WorkItems::Type.new(attributes)
end
diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb
index 5fdd0816006..fdd11b59938 100644
--- a/spec/features/abuse_report_spec.rb
+++ b/spec/features/abuse_report_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Abuse reports' do
+RSpec.describe 'Abuse reports', feature_category: :not_owned do
let(:another_user) { create(:user) }
before do
diff --git a/spec/features/action_cable_logging_spec.rb b/spec/features/action_cable_logging_spec.rb
index cf20b204cc5..c02a41c4c59 100644
--- a/spec/features/action_cable_logging_spec.rb
+++ b/spec/features/action_cable_logging_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'ActionCable logging', :js do
+RSpec.describe 'ActionCable logging', :js, feature_category: :not_owned do
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 3a02ce89aa9..10f12d7116f 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Admin::AbuseReports", :js do
+RSpec.describe "Admin::AbuseReports", :js, feature_category: :not_owned do
let(:user) { create(:user) }
context 'as an admin' do
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index b297d92b2fa..5fbe7039c1d 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin Appearance' do
+RSpec.describe 'Admin Appearance', feature_category: :not_owned do
let!(:appearance) { create(:appearance) }
let(:admin) { create(:admin) }
diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb
index b5416f539f1..a6bbdd70fc3 100644
--- a/spec/features/admin/admin_broadcast_messages_spec.rb
+++ b/spec/features/admin/admin_broadcast_messages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin Broadcast Messages' do
+RSpec.describe 'Admin Broadcast Messages', feature_category: :onboarding do
before do
admin = create(:admin)
sign_in(admin)
diff --git a/spec/features/admin/admin_browse_spam_logs_spec.rb b/spec/features/admin/admin_browse_spam_logs_spec.rb
index 471a7e8f0ab..461c9d08273 100644
--- a/spec/features/admin/admin_browse_spam_logs_spec.rb
+++ b/spec/features/admin/admin_browse_spam_logs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin browse spam logs' do
+RSpec.describe 'Admin browse spam logs', feature_category: :not_owned do
let!(:spam_log) { create(:spam_log, description: 'abcde ' * 20) }
before do
@@ -23,4 +23,11 @@ RSpec.describe 'Admin browse spam logs' do
expect(page).to have_link('Remove user')
expect(page).to have_link('Block user')
end
+
+ it 'does not perform N+1 queries' do
+ control_queries = ActiveRecord::QueryRecorder.new { visit admin_spam_logs_path }
+ create(:spam_log)
+
+ expect { visit admin_spam_logs_path }.not_to exceed_query_limit(control_queries)
+ end
end
diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb
index 56b8c7fce14..e55e1cce6b9 100644
--- a/spec/features/admin/admin_deploy_keys_spec.rb
+++ b/spec/features/admin/admin_deploy_keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'admin deploy keys', :js do
+RSpec.describe 'admin deploy keys', :js, feature_category: :authentication_and_authorization do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:admin) { create(:admin) }
diff --git a/spec/features/admin/admin_dev_ops_reports_spec.rb b/spec/features/admin/admin_dev_ops_reports_spec.rb
index f65862c568f..f290464b043 100644
--- a/spec/features/admin/admin_dev_ops_reports_spec.rb
+++ b/spec/features/admin/admin_dev_ops_reports_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'DevOps Report page', :js do
+RSpec.describe 'DevOps Report page', :js, feature_category: :devops_reports do
before do
admin = create(:admin)
sign_in(admin)
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index b370b779afe..76620b93557 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin disables Git access protocol', :js do
+RSpec.describe 'Admin disables Git access protocol', :js, feature_category: :source_code_management do
include StubENV
include MobileHelpers
diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb
index 4463dbb1eb0..eed20d449cd 100644
--- a/spec/features/admin/admin_disables_two_factor_spec.rb
+++ b/spec/features/admin/admin_disables_two_factor_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin disables 2FA for a user' do
+RSpec.describe 'Admin disables 2FA for a user', feature_category: :system_access do
include Spec::Support::Helpers::ModalHelpers
it 'successfully', :js do
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 657dd52228e..c36a742af6b 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin Groups' do
+RSpec.describe 'Admin Groups', feature_category: :subgroups do
include Select2Helper
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index 0f6cba6c105..de71a48d2dc 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Admin Health Check", :feature do
+RSpec.describe "Admin Health Check", feature_category: :continuous_verification do
include StubENV
let_it_be(:admin) { create(:admin) }
diff --git a/spec/features/admin/admin_hook_logs_spec.rb b/spec/features/admin/admin_hook_logs_spec.rb
index a2ee6343886..d6507e68692 100644
--- a/spec/features/admin/admin_hook_logs_spec.rb
+++ b/spec/features/admin/admin_hook_logs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin::HookLogs' do
+RSpec.describe 'Admin::HookLogs', feature_category: :continuous_verification do
let_it_be(:system_hook) { create(:system_hook) }
let_it_be(:hook_log) { create(:web_hook_log, web_hook: system_hook, internal_error_message: 'some error') }
let_it_be(:admin) { create(:admin) }
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index dc5b0ae009e..e6630e40147 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin::Hooks' do
+RSpec.describe 'Admin::Hooks', feature_category: :integrations do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:user) { create(:admin) }
@@ -58,10 +58,7 @@ RSpec.describe 'Admin::Hooks' do
describe 'Update existing hook' do
let(:new_url) { generate(:url) }
-
- before do
- create(:system_hook)
- end
+ let_it_be(:hook) { create(:system_hook) }
it 'updates existing hook' do
visit admin_hooks_path
@@ -71,9 +68,9 @@ RSpec.describe 'Admin::Hooks' do
check 'Enable SSL verification'
click_button 'Save changes'
- expect(page).to have_content 'SSL Verification: enabled'
- expect(page).to have_current_path(admin_hooks_path, ignore_query: true)
- expect(page).to have_content(new_url)
+ expect(page).to have_content('Enable SSL verification')
+ expect(page).to have_current_path(edit_admin_hook_path(hook), ignore_query: true)
+ expect(page).to have_content('Recent events')
end
end
@@ -145,7 +142,7 @@ RSpec.describe 'Admin::Hooks' do
visit admin_hooks_path
find('.hook-test-button.dropdown').click
- click_link 'Merge requests events'
+ click_link 'Merge request events'
expect(page).to have_content 'Hook executed successfully'
end
diff --git a/spec/features/admin/admin_jobs_spec.rb b/spec/features/admin/admin_jobs_spec.rb
index 36822f89c12..f0eaa83f05e 100644
--- a/spec/features/admin/admin_jobs_spec.rb
+++ b/spec/features/admin/admin_jobs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin Jobs' do
+RSpec.describe 'Admin Jobs', feature_category: :continuous_integration do
before do
admin = create(:admin)
sign_in(admin)
diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
index fa5c94aa66e..8d2813d26f7 100644
--- a/spec/features/admin/admin_labels_spec.rb
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'admin issues labels' do
+RSpec.describe 'admin issues labels', feature_category: :team_planning do
include Spec::Support::Helpers::ModalHelpers
let!(:bug_label) { Label.create!(title: 'bug', template: true) }
diff --git a/spec/features/admin/admin_manage_applications_spec.rb b/spec/features/admin/admin_manage_applications_spec.rb
index 4cf290293bd..b4c77e802a8 100644
--- a/spec/features/admin/admin_manage_applications_spec.rb
+++ b/spec/features/admin/admin_manage_applications_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'admin manage applications' do
+RSpec.describe 'admin manage applications', feature_category: :system_access do
let_it_be(:new_application_path) { new_admin_application_path }
let_it_be(:applications_path) { admin_applications_path }
let_it_be(:index_path) { admin_applications_path }
diff --git a/spec/features/admin/admin_mode/login_spec.rb b/spec/features/admin/admin_mode/login_spec.rb
index 6b4c9adb096..393721fe451 100644
--- a/spec/features/admin/admin_mode/login_spec.rb
+++ b/spec/features/admin/admin_mode/login_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin Mode Login' do
+RSpec.describe 'Admin Mode Login', feature_category: :authentication_and_authorization do
include TermsHelper
include UserLoginHelper
include LdapHelpers
diff --git a/spec/features/admin/admin_mode/logout_spec.rb b/spec/features/admin/admin_mode/logout_spec.rb
index 3ca66ef0d6a..f4e8941d25a 100644
--- a/spec/features/admin/admin_mode/logout_spec.rb
+++ b/spec/features/admin/admin_mode/logout_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin Mode Logout', :js do
+RSpec.describe 'Admin Mode Logout', :js, feature_category: :authentication_and_authorization do
include TermsHelper
include UserLoginHelper
include Spec::Support::Helpers::Features::TopNavSpecHelpers
diff --git a/spec/features/admin/admin_mode/workers_spec.rb b/spec/features/admin/admin_mode/workers_spec.rb
index 8405e9132b6..f3639fd0800 100644
--- a/spec/features/admin/admin_mode/workers_spec.rb
+++ b/spec/features/admin/admin_mode/workers_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
# Test an operation that triggers background jobs requiring administrative rights
-RSpec.describe 'Admin mode for workers', :request_store do
+RSpec.describe 'Admin mode for workers', :request_store, feature_category: :authentication_and_authorization do
include Spec::Support::Helpers::Features::AdminUsersHelpers
let(:user) { create(:user) }
diff --git a/spec/features/admin/admin_mode_spec.rb b/spec/features/admin/admin_mode_spec.rb
index 33cf0e8c4f8..769ff75b5a2 100644
--- a/spec/features/admin/admin_mode_spec.rb
+++ b/spec/features/admin/admin_mode_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin mode', :js do
+RSpec.describe 'Admin mode', :js, feature_category: :not_owned do
include MobileHelpers
include Spec::Support::Helpers::Features::TopNavSpecHelpers
include StubENV
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 6b147b01991..0cb813c40f4 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Admin::Projects" do
+RSpec.describe "Admin::Projects", feature_category: :projects do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
include Spec::Support::Helpers::ModalHelpers
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 92a3b388994..30fd04b1c3e 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Admin Runners" do
+RSpec.describe "Admin Runners", feature_category: :runner_fleet do
include Spec::Support::Helpers::Features::RunnersHelpers
include Spec::Support::Helpers::ModalHelpers
@@ -90,6 +90,22 @@ RSpec.describe "Admin Runners" do
end
end
+ it 'shows a running status badge that links to jobs tab' do
+ runner = create(:ci_runner, :project, projects: [project])
+ job = create(:ci_build, :running, runner: runner)
+
+ visit admin_runners_path
+
+ within_runner_row(runner.id) do
+ click_on(s_('Runners|Running'))
+ end
+
+ 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}"
+ end
+
describe 'search' do
before_all do
create(:ci_runner, :instance, description: 'runner-foo')
diff --git a/spec/features/admin/admin_search_settings_spec.rb b/spec/features/admin/admin_search_settings_spec.rb
index 989cb7cc787..3254bf75738 100644
--- a/spec/features/admin/admin_search_settings_spec.rb
+++ b/spec/features/admin/admin_search_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin searches application settings', :js do
+RSpec.describe 'Admin searches application settings', :js, feature_category: :global_search do
let_it_be(:admin) { create(:admin) }
let_it_be(:application_settings) { create(:application_setting) }
diff --git a/spec/features/admin/admin_sees_background_migrations_spec.rb b/spec/features/admin/admin_sees_background_migrations_spec.rb
index d72259d91b3..e1746dad196 100644
--- a/spec/features/admin/admin_sees_background_migrations_spec.rb
+++ b/spec/features/admin/admin_sees_background_migrations_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Admin > Admin sees background migrations" do
+RSpec.describe "Admin > Admin sees background migrations", feature_category: :database do
let_it_be(:admin) { create(:admin) }
let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob }
diff --git a/spec/features/admin/admin_sees_project_statistics_spec.rb b/spec/features/admin/admin_sees_project_statistics_spec.rb
index 9d9217c4574..d3d0625ac43 100644
--- a/spec/features/admin/admin_sees_project_statistics_spec.rb
+++ b/spec/features/admin/admin_sees_project_statistics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Admin > Admin sees project statistics" do
+RSpec.describe "Admin > Admin sees project statistics", feature_category: :projects do
let(:current_user) { create(:admin) }
before do
diff --git a/spec/features/admin/admin_sees_projects_statistics_spec.rb b/spec/features/admin/admin_sees_projects_statistics_spec.rb
index d340eb47f34..82361a985ae 100644
--- a/spec/features/admin/admin_sees_projects_statistics_spec.rb
+++ b/spec/features/admin/admin_sees_projects_statistics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Admin > Admin sees projects statistics" do
+RSpec.describe "Admin > Admin sees projects statistics", feature_category: :projects do
let(:current_user) { create(:admin) }
before do
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 72c9053ba49..2ac86ab9f49 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin updates settings' do
+RSpec.describe 'Admin updates settings', feature_category: :not_owned do
include StubENV
include TermsHelper
include UsageDataHelpers
@@ -71,11 +71,19 @@ RSpec.describe 'Admin updates settings' do
it 'change Visibility and Access Controls' do
page.within('.as-visibility-access') do
- uncheck 'Enabled'
+ page.within('[data-testid="project-export"]') do
+ uncheck 'Enabled'
+ end
+
+ page.within('[data-testid="bulk-import"]') do
+ check 'Enabled'
+ end
+
click_button 'Save changes'
end
expect(current_settings.project_export_enabled).to be_falsey
+ expect(current_settings.bulk_import_enabled).to be(true)
expect(page).to have_content "Application settings saved successfully"
end
diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb
index 8ff31dfded7..6c4a316ae77 100644
--- a/spec/features/admin/admin_system_info_spec.rb
+++ b/spec/features/admin/admin_system_info_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin System Info' do
+RSpec.describe 'Admin System Info', feature_category: :not_owned do
before do
admin = create(:admin)
sign_in(admin)
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index d93dac4834e..5e6cc206883 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin > Users > Impersonation Tokens', :js do
+RSpec.describe 'Admin > Users > Impersonation Tokens', :js, feature_category: :authentication_and_authorization do
include Spec::Support::Helpers::ModalHelpers
include Spec::Support::Helpers::AccessTokenHelpers
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index f4b7fa45e4f..1f40f1f1bce 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Admin::Users" do
+RSpec.describe "Admin::Users", feature_category: :user_management do
let(:current_user) { create(:admin) }
before do
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index 2dffef93600..318572a7664 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin uses repository checks', :request_store do
+RSpec.describe 'Admin uses repository checks', :request_store, feature_category: :user_management do
include StubENV
include Spec::Support::Helpers::ModalHelpers
diff --git a/spec/features/admin/dashboard_spec.rb b/spec/features/admin/dashboard_spec.rb
index e7ff8c23a8c..baca60134b9 100644
--- a/spec/features/admin/dashboard_spec.rb
+++ b/spec/features/admin/dashboard_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'admin visits dashboard' do
gitlab_enable_admin_mode_sign_in(admin)
end
- context 'counting forks', :js do
+ context 'counting forks', :js, feature_category: :source_code_management do
it 'correctly counts 2 forks of a project' do
project = create(:project)
project_fork = fork_project(project)
@@ -28,7 +28,7 @@ RSpec.describe 'admin visits dashboard' do
end
end
- describe 'Users statistic' do
+ describe 'Users statistic', feature_category: :user_management do
let_it_be(:users_statistics) { create(:users_statistics) }
it 'shows correct amounts of users', :aggregate_failures do
@@ -54,7 +54,7 @@ RSpec.describe 'admin visits dashboard' do
end
end
- describe 'Version check', :js do
+ describe 'Version check', :js, feature_category: :deployment_management do
it 'shows badge on CE' do
visit admin_root_path
diff --git a/spec/features/admin/integrations/instance_integrations_spec.rb b/spec/features/admin/integrations/instance_integrations_spec.rb
index 7b326ec161c..3b2ed1d9810 100644
--- a/spec/features/admin/integrations/instance_integrations_spec.rb
+++ b/spec/features/admin/integrations/instance_integrations_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Instance integrations', :js do
+RSpec.describe 'Instance integrations', :js, feature_category: :integrations do
include_context 'instance integration activation'
it_behaves_like 'integration settings form' do
diff --git a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
index 22a27b33671..d0ca5d76cc7 100644
--- a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'User activates the instance-level Mattermost Slash Command integration', :js do
+RSpec.describe 'User activates the instance-level Mattermost Slash Command integration', :js,
+feature_category: :integrations do
include_context 'instance integration activation'
before do
diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb
index 35b5c755b66..1552d4e6187 100644
--- a/spec/features/admin/users/user_spec.rb
+++ b/spec/features/admin/users/user_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin::Users::User' do
+RSpec.describe 'Admin::Users::User', feature_category: :user_management do
include Spec::Support::Helpers::Features::AdminUsersHelpers
include Spec::Support::Helpers::ModalHelpers
diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb
index 9c59f0226e0..4b49e8f4bc6 100644
--- a/spec/features/admin/users/users_spec.rb
+++ b/spec/features/admin/users/users_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin::Users' do
+RSpec.describe 'Admin::Users', feature_category: :user_management do
include Spec::Support::Helpers::Features::AdminUsersHelpers
include Spec::Support::Helpers::ModalHelpers
@@ -604,8 +604,8 @@ RSpec.describe 'Admin::Users' do
def sort_by(option)
page.within('.filtered-search-block') do
- find('.gl-new-dropdown').click
- find('.gl-new-dropdown-item', text: option).click
+ find('.gl-dropdown').click
+ find('.gl-dropdown-item', text: option).click
end
end
end
diff --git a/spec/features/admin_variables_spec.rb b/spec/features/admin_variables_spec.rb
index 9ec22bbe948..d1adbf59984 100644
--- a/spec/features/admin_variables_spec.rb
+++ b/spec/features/admin_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Instance variables', :js do
+RSpec.describe 'Instance variables', :js, feature_category: :pipeline_authoring do
let(:admin) { create(:admin) }
let(:page_path) { ci_cd_admin_application_settings_path }
diff --git a/spec/features/alert_management/alert_details_spec.rb b/spec/features/alert_management/alert_details_spec.rb
index 579b8221041..45fa4d810aa 100644
--- a/spec/features/alert_management/alert_details_spec.rb
+++ b/spec/features/alert_management/alert_details_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Alert details', :js do
+RSpec.describe 'Alert details', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:alert) { create(:alert_management_alert, project: project, status: 'triggered', title: 'Alert') }
@@ -30,6 +30,8 @@ RSpec.describe 'Alert details', :js do
alert_tabs = find('[data-testid="alertDetailsTabs"]')
expect(alert_tabs).to have_content('Alert details')
+ expect(alert_tabs).to have_content('Metrics')
+ expect(alert_tabs).to have_content('Activity feed')
end
end
@@ -61,7 +63,7 @@ RSpec.describe 'Alert details', :js do
expect(alert_status).to have_content('Triggered')
find('.gl-button').click
- find('.gl-new-dropdown-item', text: 'Acknowledged').click
+ find('.gl-dropdown-item', text: 'Acknowledged').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 2fbce27033e..6ed3bdec5f5 100644
--- a/spec/features/alert_management/alert_management_list_spec.rb
+++ b/spec/features/alert_management/alert_management_list_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Alert Management index', :js do
+RSpec.describe 'Alert Management index', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
diff --git a/spec/features/alert_management/user_filters_alerts_by_status_spec.rb b/spec/features/alert_management/user_filters_alerts_by_status_spec.rb
index bebbbcbf5f7..c3dab05550e 100644
--- a/spec/features/alert_management/user_filters_alerts_by_status_spec.rb
+++ b/spec/features/alert_management/user_filters_alerts_by_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User filters Alert Management table by status', :js do
+RSpec.describe 'User filters Alert Management table by status', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:alert1, reload: true) { create(:alert_management_alert, :triggered, project: project) }
diff --git a/spec/features/alert_management/user_searches_alerts_spec.rb b/spec/features/alert_management/user_searches_alerts_spec.rb
index 3bb1b260f36..d1e400f4145 100644
--- a/spec/features/alert_management/user_searches_alerts_spec.rb
+++ b/spec/features/alert_management/user_searches_alerts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches Alert Management alerts', :js do
+RSpec.describe 'User searches Alert Management alerts', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:alert) { create(:alert_management_alert, project: project, status: 'triggered') }
diff --git a/spec/features/alert_management/user_updates_alert_status_spec.rb b/spec/features/alert_management/user_updates_alert_status_spec.rb
index 2d7be3a0022..98fd7449c4f 100644
--- a/spec/features/alert_management/user_updates_alert_status_spec.rb
+++ b/spec/features/alert_management/user_updates_alert_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User updates Alert Management status', :js do
+RSpec.describe 'User updates Alert Management status', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:alert) { create(:alert_management_alert, project: project, status: 'triggered') }
diff --git a/spec/features/alert_management_spec.rb b/spec/features/alert_management_spec.rb
index 3322c9c574f..de6b385b4cd 100644
--- a/spec/features/alert_management_spec.rb
+++ b/spec/features/alert_management_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Alert management', :js do
+RSpec.describe 'Alert management', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
diff --git a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb
index 60f2f776595..70223b2c0d4 100644
--- a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb
+++ b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Alert integrations settings form', :js do
+RSpec.describe 'Alert integrations settings form', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:developer) { create(:user) }
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 855c91f70d7..4e204224773 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Dashboard Issues Feed" do
+RSpec.describe "Dashboard Issues Feed", feature_category: :devops_reports do
describe "GET /issues" do
let!(:user) do
user = create(:user, email: 'private1@example.com')
diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb
index 851ae7b02a0..2e9005712bb 100644
--- a/spec/features/atom/dashboard_spec.rb
+++ b/spec/features/atom/dashboard_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Dashboard Feed" do
+RSpec.describe "Dashboard Feed", feature_category: :devops_reports do
describe "GET /" do
let!(:user) { create(:user, name: "Jonh") }
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 913f5a7bcf3..89db70c6680 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issues Feed' do
+RSpec.describe 'Issues Feed', feature_category: :devops_reports do
describe 'GET /issues' do
let_it_be_with_reload(:user) do
user = create(:user, email: 'private1@example.com')
diff --git a/spec/features/atom/merge_requests_spec.rb b/spec/features/atom/merge_requests_spec.rb
index 48db8fcbf1e..b9e1c7042b2 100644
--- a/spec/features/atom/merge_requests_spec.rb
+++ b/spec/features/atom/merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Requests Feed' do
+RSpec.describe 'Merge Requests Feed', feature_category: :devops_reports do
describe 'GET /merge_requests' do
let_it_be_with_reload(:user) { create(:user, email: 'private1@example.com') }
let_it_be(:assignee) { create(:user, email: 'private2@example.com') }
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index ab874408e55..b743f900ae7 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "User Feed" do
+RSpec.describe "User Feed", feature_category: :devops_reports do
describe "GET /" do
let!(:user) { create(:user) }
diff --git a/spec/features/boards/board_filters_spec.rb b/spec/features/boards/board_filters_spec.rb
index eab92de7e8a..dee63be8119 100644
--- a/spec/features/boards/board_filters_spec.rb
+++ b/spec/features/boards/board_filters_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue board filters', :js do
+RSpec.describe 'Issue board filters', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:board) { create(:board, project: project) }
@@ -191,7 +191,7 @@ RSpec.describe 'Issue board filters', :js do
end
def expect_filtered_search_dropdown_results(filter_dropdown, count)
- expect(filter_dropdown).to have_selector('.gl-new-dropdown-item', count: count)
+ expect(filter_dropdown).to have_selector('.gl-dropdown-item', count: count)
end
def visit_project_board
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index fee9b5b378e..3e2e391d060 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -2,16 +2,31 @@
require 'spec_helper'
-RSpec.describe 'Project issue boards', :js do
+# Flaky spec warning: the queries in this file routinely exceed the defined GraphQL query limit of 100.
+# Until those queries are optimized, we need to disable query limit checking in order for these tests
+# to pass consistently. Note that removing the disabling code can lead to flaky failures locally and in CI.
+#
+# In addition, it seems as though the use of `let_it_be` might be causing some of the
+# flakiness, as discussed in https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#modifiers.
+# `reload: true` has been added to all `let_it_be` statements.
+#
+# See:
+# - https://gitlab.com/gitlab-org/gitlab/-/issues/323426
+# - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56458#note_535900110
+# - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102719
+# - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105849
+# - https://gitlab.com/gitlab-org/gitlab/-/issues/383970
+#
+RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
include DragTo
include MobileHelpers
include BoardHelpers
- let_it_be(:group) { create(:group, :nested) }
- let_it_be(:project) { create(:project, :public, namespace: group) }
- let_it_be(:board) { create(:board, project: project) }
- let_it_be(:user) { create(:user) }
- let_it_be(:user2) { create(:user) }
+ let_it_be(:group, reload: true) { create(:group, :nested) }
+ let_it_be(:project, reload: true) { create(:project, :public, namespace: group) }
+ let_it_be(:board, reload: true) { create(:board, project: project) }
+ 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(:filter_input) { find('.gl-filtered-search-term-input') }
@@ -47,34 +62,34 @@ RSpec.describe 'Project issue boards', :js do
end
context 'with lists' do
- let_it_be(:milestone) { create(:milestone, project: project) }
-
- let_it_be(:planning) { create(:label, project: project, name: 'Planning', description: 'Test') }
- let_it_be(:development) { create(:label, project: project, name: 'Development') }
- let_it_be(:testing) { create(:label, project: project, name: 'Testing') }
- let_it_be(:bug) { create(:label, project: project, name: 'Bug') }
- let_it_be(:backlog) { create(:label, project: project, name: 'Backlog') }
- let_it_be(:closed) { create(:label, project: project, name: 'Closed') }
- let_it_be(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') }
- let_it_be(:a_plus) { create(:label, project: project, name: 'A+') }
- let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) }
- let_it_be(:list2) { create(:list, board: board, label: development, position: 1) }
- let_it_be(:backlog_list) { create(:backlog_list, board: board) }
-
- let_it_be(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
- let_it_be(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) }
- let_it_be(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) }
- let_it_be(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) }
- let_it_be(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) }
- let_it_be(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) }
- let_it_be(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) }
- let_it_be(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) }
- let_it_be(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') }
- let_it_be(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) }
- let_it_be(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) }
+ let_it_be(:milestone, reload: true) { create(:milestone, project: project) }
+
+ let_it_be(:planning, reload: true) { create(:label, project: project, name: 'Planning', description: 'Test') }
+ let_it_be(:development, reload: true) { create(:label, project: project, name: 'Development') }
+ let_it_be(:testing, reload: true) { create(:label, project: project, name: 'Testing') }
+ let_it_be(:bug, reload: true) { create(:label, project: project, name: 'Bug') }
+ let_it_be(:backlog, reload: true) { create(:label, project: project, name: 'Backlog') }
+ let_it_be(:closed, reload: true) { create(:label, project: project, name: 'Closed') }
+ let_it_be(:accepting, reload: true) { create(:label, project: project, name: 'Accepting Merge Requests') }
+ let_it_be(:a_plus, reload: true) { create(:label, project: project, name: 'A+') }
+ let_it_be(:list1, reload: true) { create(:list, board: board, label: planning, position: 0) }
+ let_it_be(:list2, reload: true) { create(:list, board: board, label: development, position: 1) }
+ let_it_be(:backlog_list, reload: true) { create(:backlog_list, board: board) }
+
+ let_it_be(:confidential_issue, reload: true) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
+ let_it_be(:issue1, reload: true) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) }
+ let_it_be(:issue2, reload: true) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) }
+ let_it_be(:issue3, reload: true) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) }
+ let_it_be(:issue4, reload: true) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) }
+ let_it_be(:issue5, reload: true) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) }
+ let_it_be(:issue6, reload: true) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) }
+ let_it_be(:issue7, reload: true) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) }
+ let_it_be(:issue8, reload: true) { create(:closed_issue, project: project, title: 'hhh', description: '888') }
+ let_it_be(:issue9, reload: true) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) }
+ let_it_be(:issue10, reload: true) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) }
before do
- visit_project_board(project, board)
+ visit_project_board_path_without_query_limit(project, board)
end
it 'shows description tooltip on list title', :quarantine do
@@ -88,7 +103,7 @@ RSpec.describe 'Project issue boards', :js do
wait_for_board_cards(3, 2)
end
- it 'shows confidential issues with icon' do
+ it 'shows confidential issues with icon', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383970' do
page.within(find('.board:nth-child(2)')) do
expect(page).to have_selector('.confidential-icon', count: 1)
end
@@ -125,7 +140,7 @@ RSpec.describe 'Project issue boards', :js do
it 'infinite scrolls list' do
create_list(:labeled_issue, 30, project: project, labels: [planning])
- visit_project_board(project, board)
+ visit_project_board_path_without_query_limit(project, board)
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('38')
@@ -164,7 +179,7 @@ RSpec.describe 'Project issue boards', :js do
end
end
- context 'closed' do
+ context 'closed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383970' do
it 'shows list of closed issues' do
wait_for_board_cards(4, 1)
wait_for_requests
@@ -204,31 +219,26 @@ RSpec.describe 'Project issue boards', :js do
expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title)
# Make sure list positions are preserved after a reload
- visit_project_board(project, board)
+ visit_project_board_path_without_query_limit(project, board)
expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(development.title)
expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title)
end
context 'without backlog and closed lists' do
- let_it_be(:board) { create(:board, project: project, hide_backlog_list: true, hide_closed_list: true) }
- let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) }
- let_it_be(:list2) { create(:list, board: board, label: development, position: 1) }
+ let_it_be(:board, reload: true) { create(:board, project: project, hide_backlog_list: true, hide_closed_list: true) }
+ let_it_be(:list1, reload: true) { create(:list, board: board, label: planning, position: 0) }
+ let_it_be(:list2, reload: true) { create(:list, board: board, label: development, position: 1) }
it 'changes position of list' do
- inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
- visit_project_board(project, board)
- end
+ visit_project_board_path_without_query_limit(project, board)
drag(list_from_index: 0, list_to_index: 1, selector: '.board-header')
expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title)
expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title)
- inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
- # Make sure list positions are preserved after a reload
- visit_project_board(project, board)
- end
+ visit_project_board_path_without_query_limit(project, board)
expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title)
expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title)
@@ -246,7 +256,7 @@ RSpec.describe 'Project issue boards', :js do
expect(page).to have_selector(selector, text: development.title, count: 1)
end
- it 'issue moves between lists and does not show the "Development" label since the card is in the "Development" list label' do
+ it 'issue moves between lists and does not show the "Development" label since the card is in the "Development" list label', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383970' do
drag(list_from_index: 1, from_index: 1, list_to_index: 2)
wait_for_board_cards(2, 7)
@@ -257,7 +267,7 @@ RSpec.describe 'Project issue boards', :js do
expect(find('.board:nth-child(3)').all('.board-card').last).not_to have_content(development.title)
end
- it 'issue moves between lists and does not show the "Planning" label since the card is in the "Planning" list label' do
+ it 'issue moves between lists and does not show the "Planning" label since the card is in the "Planning" list label', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383970' do
drag(list_from_index: 2, list_to_index: 1)
wait_for_board_cards(2, 9)
@@ -268,7 +278,7 @@ RSpec.describe 'Project issue boards', :js do
expect(find('.board:nth-child(2)').all('.board-card').first).not_to have_content(planning.title)
end
- it 'issue moves from closed' do
+ it 'issue moves from closed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383970' do
drag(list_from_index: 2, list_to_index: 3)
wait_for_board_cards(2, 8)
@@ -285,7 +295,7 @@ RSpec.describe 'Project issue boards', :js do
end
end
- context 'list header' do
+ context 'list header', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383970' do
let(:total_planning_issues) { "8" }
it 'shows issue count on the list' do
@@ -309,7 +319,7 @@ RSpec.describe 'Project issue boards', :js do
wait_for_empty_boards((3..4))
end
- it 'filters by assignee' do
+ it 'filters by assignee', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383970' do
set_filter("assignee", user.username)
click_on user.username
filter_submit.click
@@ -331,7 +341,7 @@ RSpec.describe 'Project issue boards', :js do
wait_for_board_cards(4, 0)
end
- it 'filters by label' do
+ it 'filters by label', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383970' do
set_filter("label", testing.title)
click_on testing.title
filter_submit.click
@@ -390,7 +400,7 @@ RSpec.describe 'Project issue boards', :js do
wait_for_board_cards(2, 8)
end
- it 'infinite scrolls list with label filter' do
+ it 'infinite scrolls list with label filter', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383970' do
create_list(:labeled_issue, 30, project: project, labels: [planning, testing])
set_filter("label", testing.title)
@@ -531,7 +541,7 @@ RSpec.describe 'Project issue boards', :js do
end
context 'as guest user' do
- let_it_be(:user_guest) { create(:user) }
+ let_it_be(:user_guest, reload: true) { create(:user) }
before do
stub_feature_flags(apollo_boards: false)
@@ -601,4 +611,10 @@ RSpec.describe 'Project issue boards', :js do
wait_for_requests
end
+
+ def visit_project_board_path_without_query_limit(project, board)
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ visit_project_board(project, board)
+ end
+ end
end
diff --git a/spec/features/boards/focus_mode_spec.rb b/spec/features/boards/focus_mode_spec.rb
index 453a8d8870b..8f3ce25b583 100644
--- a/spec/features/boards/focus_mode_spec.rb
+++ b/spec/features/boards/focus_mode_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue Boards focus mode', :js do
+RSpec.describe 'Issue Boards focus mode', :js, feature_category: :team_planning do
let(:project) { create(:project, :public) }
before do
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index a3dda3b9d2f..f1ee7a8fde7 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue Boards', :js do
+RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do
include DragTo
let(:project) { create(:project, :public) }
diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb
index cefb486349d..6f03f6db3ab 100644
--- a/spec/features/boards/keyboard_shortcut_spec.rb
+++ b/spec/features/boards/keyboard_shortcut_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue Boards shortcut', :js do
+RSpec.describe 'Issue Boards shortcut', :js, feature_category: :team_planning do
context 'issues are enabled' do
let(:project) { create(:project) }
diff --git a/spec/features/boards/multi_select_spec.rb b/spec/features/boards/multi_select_spec.rb
index cad303a14e5..7afe34be3d8 100644
--- a/spec/features/boards/multi_select_spec.rb
+++ b/spec/features/boards/multi_select_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Multi Select Issue', :js do
+RSpec.describe 'Multi Select Issue', :js, feature_category: :team_planning do
include DragTo
let(:group) { create(:group, :nested) }
diff --git a/spec/features/boards/multiple_boards_spec.rb b/spec/features/boards/multiple_boards_spec.rb
index 219f24f60d7..e9d34c6f87f 100644
--- a/spec/features/boards/multiple_boards_spec.rb
+++ b/spec/features/boards/multiple_boards_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Multiple Issue Boards', :js do
+RSpec.describe 'Multiple Issue Boards', :js, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:planning) { create(:label, project: project, name: 'Planning') }
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 5f4517d47ee..1b0695e4e60 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue Boards new issue', :js do
+RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public) }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:backlog_list) { create(:backlog_list, board: board) }
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 7fa440befc1..0ca680c5ed5 100644
--- a/spec/features/boards/reload_boards_on_browser_back_spec.rb
+++ b/spec/features/boards/reload_boards_on_browser_back_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Ensure Boards do not show stale data on browser back', :js do
+RSpec.describe 'Ensure Boards do not show stale data on browser back', :js, feature_category: :team_planning do
let(:project) { create(:project, :public) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
diff --git a/spec/features/boards/sidebar_assignee_spec.rb b/spec/features/boards/sidebar_assignee_spec.rb
index 63553cec89b..e3de594f856 100644
--- a/spec/features/boards/sidebar_assignee_spec.rb
+++ b/spec/features/boards/sidebar_assignee_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Project issue boards sidebar assignee', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332230' do
+RSpec.describe 'Project issue boards sidebar assignee', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332230',
+ feature_category: :team_planning do
include BoardHelpers
let_it_be(:user) { create(:user) }
@@ -112,7 +113,7 @@ RSpec.describe 'Project issue boards sidebar assignee', :js, quarantine: 'https:
page.within(assignees_widget) do
click_button('Edit')
- expect(find('.dropdown-menu')).to have_selector('.gl-new-dropdown-item-check-icon')
+ expect(find('.dropdown-menu')).to have_selector('.gl-dropdown-item-check-icon')
end
end
end
diff --git a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
index 8395a0b33c0..c3bb58df797 100644
--- a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
+++ b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue boards sidebar labels select', :js do
+RSpec.describe 'Issue boards sidebar labels select', :js, feature_category: :team_planning do
include BoardHelpers
include_context 'labels from nested groups and projects'
diff --git a/spec/features/boards/sidebar_labels_spec.rb b/spec/features/boards/sidebar_labels_spec.rb
index 12d91e9c5a8..460d0d232b3 100644
--- a/spec/features/boards/sidebar_labels_spec.rb
+++ b/spec/features/boards/sidebar_labels_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project issue boards sidebar labels', :js do
+RSpec.describe 'Project issue boards sidebar labels', :js, feature_category: :team_planning do
include BoardHelpers
let_it_be(:group) { create(:group, :public) }
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 2b2a412194a..0a16e95c0bf 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project issue boards sidebar', :js do
+RSpec.describe 'Project issue boards sidebar', :js, feature_category: :team_planning do
include BoardHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/boards/user_adds_lists_to_board_spec.rb b/spec/features/boards/user_adds_lists_to_board_spec.rb
index 480a88a6b84..a936e14168c 100644
--- a/spec/features/boards/user_adds_lists_to_board_spec.rb
+++ b/spec/features/boards/user_adds_lists_to_board_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User adds lists', :js do
+RSpec.describe 'User adds lists', :js, feature_category: :team_planning do
let_it_be(:group) { create(:group, :nested) }
let_it_be(:project) { create(:project, :public, namespace: group) }
let_it_be(:group_board) { create(:board, group: group) }
diff --git a/spec/features/boards/user_visits_board_spec.rb b/spec/features/boards/user_visits_board_spec.rb
index c386477fa9d..44c691435df 100644
--- a/spec/features/boards/user_visits_board_spec.rb
+++ b/spec/features/boards/user_visits_board_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User visits issue boards', :js do
+RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning do
using RSpec::Parameterized::TableSyntax
let_it_be(:group) { create_default(:group, :public) }
diff --git a/spec/features/breadcrumbs_schema_markup_spec.rb b/spec/features/breadcrumbs_schema_markup_spec.rb
index f86ad5cd2ae..d924423c9a9 100644
--- a/spec/features/breadcrumbs_schema_markup_spec.rb
+++ b/spec/features/breadcrumbs_schema_markup_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Breadcrumbs schema markup', :aggregate_failures do
+RSpec.describe 'Breadcrumbs schema markup', :aggregate_failures, feature_category: :not_owned do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, namespace: user.namespace) }
let_it_be(:issue) { create(:issue, project: project) }
diff --git a/spec/features/broadcast_messages_spec.rb b/spec/features/broadcast_messages_spec.rb
index 1fec68a1d98..8300cfce539 100644
--- a/spec/features/broadcast_messages_spec.rb
+++ b/spec/features/broadcast_messages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Broadcast Messages' do
+RSpec.describe 'Broadcast Messages', feature_category: :onboarding do
let_it_be(:user) { create(:user) }
shared_examples 'a Broadcast Messages' do |type|
@@ -31,11 +31,13 @@ RSpec.describe 'Broadcast Messages' do
expect(page).not_to have_content 'SampleMessage'
end
- it 'broadcast message is still hidden after refresh', :js,
- quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347118' do
+ it 'broadcast message is still hidden after refresh', :js do
visit root_path
find('.js-dismiss-current-broadcast-notification').click
+
+ wait_for_cookie_set("hide_broadcast_message_#{broadcast_message.id}")
+
visit root_path
expect(page).not_to have_content 'SampleMessage'
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index a8aa3f0b36a..2c5b7d66e2f 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Contributions Calendar', :js do
+RSpec.describe 'Contributions Calendar', :js, feature_category: :users do
include MobileHelpers
let(:user) { create(:user) }
@@ -143,18 +143,27 @@ RSpec.describe 'Contributions Calendar', :js do
end
end
- describe '1 issue creation calendar activity' do
+ describe '1 issue and 1 work item creation calendar activity' do
before do
Issues::CreateService.new(project: contributed_project, current_user: user, params: issue_params, spam_params: nil).execute
+ WorkItems::CreateService.new(
+ project: contributed_project,
+ current_user: user,
+ params: { title: 'new task' },
+ spam_params: nil
+ ).execute
end
- it_behaves_like 'a day with activity', contribution_count: 1
+ it_behaves_like 'a day with activity', contribution_count: 2
describe 'issue title is shown on activity page' do
include_context 'visit user page'
- it 'displays calendar activity log', :sidekiq_might_not_need_inline do
- expect(find('#js-overview .overview-content-list .event-target-title')).to have_content issue_title
+ it 'displays calendar activity log', :sidekiq_inline do
+ expect(all('#js-overview .overview-content-list .event-target-title').map(&:text)).to contain_exactly(
+ match(/#{issue_title}/),
+ match(/new task/)
+ )
end
end
end
diff --git a/spec/features/callouts/registration_enabled_spec.rb b/spec/features/callouts/registration_enabled_spec.rb
index 79e99712183..1ea52dbf12a 100644
--- a/spec/features/callouts/registration_enabled_spec.rb
+++ b/spec/features/callouts/registration_enabled_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe 'Registration enabled callout' do
+RSpec.describe 'Registration enabled callout', feature_category: :authentication_and_authorization do
let_it_be(:admin) { create(:admin) }
let_it_be(:non_admin) { create(:user) }
let_it_be(:project) { create(:project) }
- let_it_be(:callout_title) { _('Anyone can register for an account.') }
+ let_it_be(:callout_title) { _('Check your sign-up restrictions') }
context 'when "Sign-up enabled" setting is `true`' do
before do
@@ -22,7 +22,7 @@ RSpec.describe 'Registration enabled callout' do
visit root_path
expect(page).to have_content callout_title
- expect(page).to have_link _('Turn off'), href: general_admin_application_settings_path(anchor: 'js-signup-settings')
+ expect(page).to have_link _('Deactivate'), href: general_admin_application_settings_path(anchor: 'js-signup-settings')
visit root_dashboard_path
diff --git a/spec/features/canonical_link_spec.rb b/spec/features/canonical_link_spec.rb
index 8b64e9a5b9d..d8f9a7584e7 100644
--- a/spec/features/canonical_link_spec.rb
+++ b/spec/features/canonical_link_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Canonical link' do
+RSpec.describe 'Canonical link', feature_category: :remote_development do
include Spec::Support::Helpers::Features::CanonicalLinkHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/clusters/cluster_detail_page_spec.rb b/spec/features/clusters/cluster_detail_page_spec.rb
index 06e3e00db7d..e8fb5f4105d 100644
--- a/spec/features/clusters/cluster_detail_page_spec.rb
+++ b/spec/features/clusters/cluster_detail_page_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Clusterable > Show page' do
+RSpec.describe 'Clusterable > Show page', feature_category: :kubernetes_management do
include KubernetesHelpers
let(:current_user) { create(:user) }
diff --git a/spec/features/clusters/cluster_health_dashboard_spec.rb b/spec/features/clusters/cluster_health_dashboard_spec.rb
index 88d6976c2be..b557f803a99 100644
--- a/spec/features/clusters/cluster_health_dashboard_spec.rb
+++ b/spec/features/clusters/cluster_health_dashboard_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Cluster Health board', :js, :kubeclient, :use_clean_rails_memory_store_caching, :sidekiq_inline do
+RSpec.describe 'Cluster Health board', :js, :kubeclient, :use_clean_rails_memory_store_caching, :sidekiq_inline,
+feature_category: :kubernetes_management do
include KubernetesHelpers
include PrometheusHelpers
diff --git a/spec/features/clusters/create_agent_spec.rb b/spec/features/clusters/create_agent_spec.rb
index b19e57c550c..01902c36e99 100644
--- a/spec/features/clusters/create_agent_spec.rb
+++ b/spec/features/clusters/create_agent_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Cluster agent registration', :js do
+RSpec.describe 'Cluster agent registration', :js, feature_category: :kubernetes_management do
let_it_be(:project) { create(:project, :custom_repo, files: { '.gitlab/agents/example-agent-1/config.yaml' => '' }) }
let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
let_it_be(:token) { Devise.friendly_token }
@@ -30,8 +30,8 @@ RSpec.describe 'Cluster agent registration', :js do
click_button('Connect a cluster')
expect(page).to have_content('Register')
- click_button('Select an agent')
- click_button('example-agent-2')
+ click_button('Select an agent or enter a name to create new')
+ page.find('li', text: 'example-agent-2').click
click_button('Register')
expect(page).to have_content('You cannot see this token again after you close this window.')
diff --git a/spec/features/commit_spec.rb b/spec/features/commit_spec.rb
index c9fa10d58e6..649b67e7fd0 100644
--- a/spec/features/commit_spec.rb
+++ b/spec/features/commit_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Commit' do
+RSpec.describe 'Commit', feature_category: :source_code_management do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/commits/user_uses_quick_actions_spec.rb b/spec/features/commits/user_uses_quick_actions_spec.rb
index 12e7865e490..6d043a0bb2f 100644
--- a/spec/features/commits/user_uses_quick_actions_spec.rb
+++ b/spec/features/commits/user_uses_quick_actions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Commit > User uses quick actions', :js do
+RSpec.describe 'Commit > User uses quick actions', :js, feature_category: :source_code_management do
include Spec::Support::Helpers::Features::NotesHelpers
include RepoHelpers
diff --git a/spec/features/commits/user_view_commits_spec.rb b/spec/features/commits/user_view_commits_spec.rb
index f7fd3a6e209..b58d7cf3741 100644
--- a/spec/features/commits/user_view_commits_spec.rb
+++ b/spec/features/commits/user_view_commits_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Commit > User view commits' do
+RSpec.describe 'Commit > User view commits', feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 97f820c1518..e4d4375a138 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Commits' do
+RSpec.describe 'Commits', feature_category: :source_code_management do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
@@ -183,7 +183,7 @@ RSpec.describe 'Commits' do
set_cookie('new_repo', 'true')
visit project_commits_path(project, branch_name)
- expect(find('.js-project-refs-dropdown')).to have_content branch_name
+ expect(find('.ref-selector')).to have_content branch_name
end
end
diff --git a/spec/features/contextual_sidebar_spec.rb b/spec/features/contextual_sidebar_spec.rb
index cc4a0471d4e..2b671d4b3f1 100644
--- a/spec/features/contextual_sidebar_spec.rb
+++ b/spec/features/contextual_sidebar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Contextual sidebar', :js do
+RSpec.describe 'Contextual sidebar', :js, feature_category: :remote_development do
context 'when context is a project' do
let_it_be(:project) { create(:project) }
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 8de4c66c62f..55bf77d00b1 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Value Stream Analytics', :js do
+RSpec.describe 'Value Stream Analytics', :js, feature_category: :value_stream_management do
include CycleAnalyticsHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index 7390edc3c47..b1734cb353b 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard > Activity' do
+RSpec.describe 'Dashboard > Activity', feature_category: :users do
let(:user) { create(:user) }
before do
diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb
index d157d44bab7..d3992d34506 100644
--- a/spec/features/dashboard/archived_projects_spec.rb
+++ b/spec/features/dashboard/archived_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Archived Project' do
+RSpec.describe 'Dashboard Archived Project', feature_category: :projects do
let(:user) { create :user }
let(:project) { create :project }
let(:archived_project) { create(:project, :archived) }
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index de8858fa8fa..34f99765c29 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Tooltips on .timeago dates', :js do
+RSpec.describe 'Tooltips on .timeago dates', :js, feature_category: :users do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, name: 'test', namespace: user.namespace) }
diff --git a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
index 6861fac3cc2..f5b02a87758 100644
--- a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
+++ b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'The group dashboard', :js do
+RSpec.describe 'The group dashboard', :js, feature_category: :subgroups do
include ExternalAuthorizationServiceHelpers
include Spec::Support::Helpers::Features::TopNavSpecHelpers
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index f1283d29f4c..f363007f0d7 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Group' do
+RSpec.describe 'Dashboard Group', feature_category: :subgroups do
before do
sign_in(create(:user))
end
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index 3a4296836bd..b28e2ccf787 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Groups page', :js do
+RSpec.describe 'Dashboard Groups page', :js, feature_category: :subgroups do
let(:user) { create :user }
let(:group) { create(:group) }
let(:nested_group) { create(:group, :nested) }
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index 91901414dde..5c7285f0491 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do
+RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:issue) { create(:issue, project: project) }
@@ -12,6 +12,7 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching d
issue.assignees = [user]
merge_request.update!(assignees: [user])
sign_in(user)
+ stub_feature_flags(limit_assigned_issues_count: false)
end
it 'reflects dashboard issues count' do
@@ -30,6 +31,28 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching d
end
end
+ context 'when :limit_assigned_issues_count FF is used' do
+ before do
+ stub_feature_flags(limit_assigned_issues_count: true)
+ end
+
+ it 'reflects dashboard issues count' do
+ visit issues_path
+
+ expect_counters('issues', '1', n_("%d assigned issue", "%d assigned issues", 1) % 1)
+
+ issue.update!(assignees: [])
+
+ Users::AssignedIssuesCountService.new(current_user: user).delete_cache
+
+ travel_to(3.minutes.from_now) do
+ visit issues_path
+
+ expect_counters('issues', '0', n_("%d assigned issue", "%d assigned issues", 0) % 0)
+ end
+ end
+ end
+
it 'reflects dashboard merge requests count', :js do
visit merge_requests_path
diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb
index 0d10aed955a..d5f362d8449 100644
--- a/spec/features/dashboard/issues_filter_spec.rb
+++ b/spec/features/dashboard/issues_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Issues filtering', :js do
+RSpec.describe 'Dashboard Issues filtering', :js, feature_category: :team_planning do
include Spec::Support::Helpers::Features::SortingHelpers
include FilteredSearchHelpers
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 64181041be5..d74965f58fa 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Issues' do
+RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
include FilteredSearchHelpers
let(:current_user) { create :user }
diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb
index ebe5c3e1091..f116c84ff40 100644
--- a/spec/features/dashboard/label_filter_spec.rb
+++ b/spec/features/dashboard/label_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard > label filter', :js do
+RSpec.describe 'Dashboard > label filter', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let(:filtered_search) { find('.filtered-search') }
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 70f614cdcef..56d7c45de5d 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Merge Requests' do
+RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review do
include Spec::Support::Helpers::Features::SortingHelpers
include FilteredSearchHelpers
include ProjectForksHelper
diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb
index 08cb95979ac..b4d0d9c5812 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' do
+RSpec.describe 'Dashboard > Milestones', feature_category: :team_planning do
describe 'as anonymous user' do
before do
visit dashboard_milestones_path
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index c26a1a0b486..5bf1566fa31 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project member activity', :js do
+RSpec.describe 'Project member activity', :js, feature_category: :users do
let(:user) { create(:user) }
let(:project) { create(:project, :public, name: 'x', namespace: user.namespace) }
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index c132caa88c8..2b89f16bbff 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Projects' do
+RSpec.describe 'Dashboard Projects', feature_category: :projects do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :repository) }
let_it_be(:project2) { create(:project, :public) }
@@ -101,14 +101,6 @@ RSpec.describe 'Dashboard Projects' do
expect(first('.project-row')).to have_content(project_with_most_stars.title)
end
-
- it 'shows tabs to filter by all projects or personal' do
- visit dashboard_projects_path
- segmented_button = page.find('.filtered-search-nav .button-filter-group')
-
- expect(segmented_button).to have_content 'All'
- expect(segmented_button).to have_content 'Personal'
- end
end
context 'when on Starred projects tab', :js do
diff --git a/spec/features/dashboard/root_explore_spec.rb b/spec/features/dashboard/root_explore_spec.rb
index a3c346ffe2a..c0d1f0de1f5 100644
--- a/spec/features/dashboard/root_explore_spec.rb
+++ b/spec/features/dashboard/root_explore_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Root explore' do
+RSpec.describe 'Root explore', feature_category: :not_owned do
let_it_be(:public_project) { create(:project, :public) }
let_it_be(:archived_project) { create(:project, :archived) }
let_it_be(:internal_project) { create(:project, :internal) }
@@ -30,4 +30,26 @@ RSpec.describe 'Root explore' do
include_examples 'shows public projects'
end
+
+ describe 'project language dropdown' do
+ let(:has_language_dropdown?) { page.has_selector?('[data-testid="project-language-dropdown"]') }
+
+ it 'is conditionally rendered' do
+ visit explore_projects_path
+
+ expect(has_language_dropdown?).to eq(true)
+ end
+
+ context 'with project_language_search ff disabled' do
+ before do
+ stub_feature_flags(project_language_search: false)
+ end
+
+ it 'is conditionally rendered' do
+ visit explore_projects_path
+
+ expect(has_language_dropdown?).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index 3f3ab4218f2..30587756505 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard shortcuts', :js do
+RSpec.describe 'Dashboard shortcuts', :js, feature_category: :not_owned do
context 'logged in' do
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index f891950eeb8..ab2cfc0573e 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard snippets' do
+RSpec.describe 'Dashboard snippets', feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
context 'when the project has snippets' do
diff --git a/spec/features/dashboard/todos/target_state_spec.rb b/spec/features/dashboard/todos/target_state_spec.rb
index b0aafdda59a..f8b525a63f1 100644
--- a/spec/features/dashboard/todos/target_state_spec.rb
+++ b/spec/features/dashboard/todos/target_state_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard > Todo target states' do
+RSpec.describe 'Dashboard > Todo target states', feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb
index 938e42623f6..ea8c7e800c5 100644
--- a/spec/features/dashboard/todos/todos_filtering_spec.rb
+++ b/spec/features/dashboard/todos/todos_filtering_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard > User filters todos', :js do
+RSpec.describe 'Dashboard > User filters todos', :js, feature_category: :team_planning do
let(:user_1) { create(:user, username: 'user_1', name: 'user_1') }
let(:user_2) { create(:user, username: 'user_2', name: 'user_2') }
@@ -58,9 +58,9 @@ RSpec.describe 'Dashboard > User filters todos', :js do
wait_for_requests
- expect(page).to have_content "issue #{issue1.to_reference} \"issue\" at #{group1.name} / project_1"
- expect(page).to have_content "merge request #{merge_request.to_reference}"
- expect(page).not_to have_content "issue #{issue2.to_reference} \"issue\" at #{group2.name} / project_3"
+ expect(page).to have_content "issue · #{group1.name} / project_1 #{issue1.to_reference}"
+ expect(page).to have_content merge_request.to_reference.to_s
+ expect(page).not_to have_content "issue · #{group2.name} / project_3 #{issue2.to_reference}"
end
context 'Author filter' do
@@ -74,8 +74,8 @@ RSpec.describe 'Dashboard > User filters todos', :js do
wait_for_requests
- expect(find('.todos-list')).to have_content 'merge request'
- expect(find('.todos-list')).not_to have_content 'issue'
+ expect(find('.todos-list')).to have_content '!'
+ expect(find('.todos-list')).not_to have_content '#'
end
it 'shows only authors of existing todos' do
@@ -174,11 +174,11 @@ RSpec.describe 'Dashboard > User filters todos', :js do
def expect_to_see_action(action_name)
action_names = {
- assigned: ' assigned you ',
- review_requested: ' requested a review of ',
- mentioned: ' mentioned ',
- marked: ' added a todo for ',
- build_failed: ' pipeline failed in '
+ assigned: ' assigned you',
+ review_requested: ' requested a review',
+ mentioned: ' mentioned',
+ marked: ' added a to-do item',
+ build_failed: ' pipeline failed'
}
action_name_text = action_names.delete(action_name)
diff --git a/spec/features/dashboard/todos/todos_sorting_spec.rb b/spec/features/dashboard/todos/todos_sorting_spec.rb
index a0fa53b761b..e449f71878b 100644
--- a/spec/features/dashboard/todos/todos_sorting_spec.rb
+++ b/spec/features/dashboard/todos/todos_sorting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard > User sorts todos' do
+RSpec.describe 'Dashboard > User sorts todos', feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index e02cd182b2c..606bc82a7bb 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Todos' do
+RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
include DesignManagementTestHelpers
let_it_be(:user) { create(:user, username: 'john') }
@@ -49,29 +49,8 @@ RSpec.describe 'Dashboard Todos' do
visit dashboard_todos_path
end
- it 'renders the mr link with the extra attributes' do
- link = page.find_link(referenced_mr.to_reference)
-
- expect(link).not_to be_nil
- expect(link['data-iid']).to eq(referenced_mr.iid.to_s)
- expect(link['data-project-path']).to eq(referenced_mr.project.full_path)
- expect(link['title']).to eq(referenced_mr.title)
- expect(link['data-reference-type']).to eq('merge_request')
- end
- end
-
- context 'when todo references an issue of type task' do
- let(:task) { create(:issue, :task, project: project) }
- let!(:task_todo) { create(:todo, :mentioned, user: user, project: project, target: task, author: author) }
-
- before do
- sign_in(user)
-
- visit dashboard_todos_path
- end
-
- it 'displays the correct issue type name' do
- expect(page).to have_content('mentioned you on task')
+ it 'renders the mr reference' do
+ expect(page).to have_content(referenced_mr.to_reference)
end
end
@@ -100,10 +79,6 @@ RSpec.describe 'Dashboard Todos' do
visit dashboard_todos_path
end
- it 'displays the correct issue type name' do
- expect(page).to have_content('mentioned you on issue')
- end
-
it 'has todo present' do
expect(page).to have_selector('.todos-list .todo', count: 1)
end
@@ -117,7 +92,7 @@ RSpec.describe 'Dashboard Todos' do
shared_examples 'deleting the todo' do
before do
within first('.todo') do
- click_link 'Done'
+ find('[data-testid="check-icon"]').click
end
end
@@ -143,9 +118,9 @@ RSpec.describe 'Dashboard Todos' do
shared_examples 'deleting and restoring the todo' do
before do
within first('.todo') do
- click_link 'Done'
+ find('[data-testid="check-icon"]').click
wait_for_requests
- click_link 'Undo'
+ find('[data-testid="redo-icon"]').click
end
end
@@ -192,7 +167,8 @@ RSpec.describe 'Dashboard Todos' do
it 'shows issue assigned to yourself message' do
page.within('.js-todos-all') do
- expect(page).to have_content("You assigned issue #{issue.to_reference} \"Fix bug\" at #{project.namespace.owner_name} / #{project.name} to yourself")
+ expect(page).to have_content("Fix bug · #{project.namespace.owner_name} / #{project.name} #{issue.to_reference}")
+ expect(page).to have_content("You assigned to yourself.")
end
end
end
@@ -203,10 +179,10 @@ RSpec.describe 'Dashboard Todos' do
visit dashboard_todos_path
end
- it 'shows you added a todo message' do
+ it 'shows you added a to-do item message' do
page.within('.js-todos-all') do
- expect(page).to have_content("You added a todo for issue #{issue.to_reference} \"Fix bug\" at #{project.namespace.owner_name} / #{project.name}")
- expect(page).not_to have_content('to yourself')
+ expect(page).to have_content("Fix bug · #{project.namespace.owner_name} / #{project.name} #{issue.to_reference}")
+ expect(page).to have_content("You added a to-do item.")
end
end
end
@@ -219,8 +195,8 @@ RSpec.describe 'Dashboard Todos' do
it 'shows you mentioned yourself message' do
page.within('.js-todos-all') do
- expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference} \"Fix bug\" at #{project.namespace.owner_name} / #{project.name}")
- expect(page).not_to have_content('to yourself')
+ expect(page).to have_content("Fix bug · #{project.namespace.owner_name} / #{project.name} #{issue.to_reference}")
+ expect(page).to have_content("You mentioned yourself.")
end
end
end
@@ -233,8 +209,8 @@ RSpec.describe 'Dashboard Todos' do
it 'shows you directly addressed yourself message being displayed as mentioned yourself' do
page.within('.js-todos-all') do
- expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference} \"Fix bug\" at #{project.namespace.owner_name} / #{project.name}")
- expect(page).not_to have_content('to yourself')
+ expect(page).to have_content("Fix bug · #{project.namespace.owner_name} / #{project.name} #{issue.to_reference}")
+ expect(page).to have_content("You mentioned yourself.")
end
end
end
@@ -249,8 +225,8 @@ RSpec.describe 'Dashboard Todos' do
it 'shows you set yourself as an approver message' do
page.within('.js-todos-all') do
- expect(page).to have_content("You set yourself as an approver for merge request #{merge_request.to_reference} \"Fixes issue\" at #{project.namespace.owner_name} / #{project.name}")
- expect(page).not_to have_content('to yourself')
+ expect(page).to have_content("Fixes issue · #{project.namespace.owner_name} / #{project.name} #{merge_request.to_reference}")
+ expect(page).to have_content("You set yourself as an approver.")
end
end
end
@@ -265,7 +241,28 @@ RSpec.describe 'Dashboard Todos' do
it 'shows you set yourself as an reviewer message' do
page.within('.js-todos-all') do
- expect(page).to have_content("You requested a review of merge request #{merge_request.to_reference} \"Fixes issue\" at #{project.namespace.owner_name} / #{project.name} from yourself")
+ expect(page).to have_content("Fixes issue · #{project.namespace.owner_name} / #{project.name} #{merge_request.to_reference}")
+ expect(page).to have_content("You requested a review from yourself.")
+ end
+ end
+ end
+ end
+
+ context 'User has automatically created todos' do
+ before do
+ sign_in(user)
+ end
+
+ context 'unmergeable todo' do
+ before do
+ create(:todo, :unmergeable, user: user, project: project, target: issue, author: user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows unmergeable message' do
+ page.within('.js-todos-all') do
+ expect(page).to have_content("Fix bug · #{project.namespace.owner_name} / #{project.name} #{issue.to_reference}")
+ expect(page).to have_content("Could not merge.")
end
end
end
@@ -285,7 +282,7 @@ RSpec.describe 'Dashboard Todos' do
describe 'restoring the todo' do
before do
within first('.todo') do
- click_link 'Add a to do'
+ find('[data-testid="todo-add-icon"]').click
end
end
@@ -391,7 +388,7 @@ RSpec.describe 'Dashboard Todos' do
context 'User has deleted a todo' do
before do
within first('.todo') do
- click_link 'Done'
+ find('[data-testid="check-icon"]').click
end
end
@@ -420,13 +417,7 @@ RSpec.describe 'Dashboard Todos' do
end
it 'shows the todo' do
- expect(page).to have_content 'The pipeline failed in merge request'
- end
-
- it 'links to the pipelines for the merge request' do
- href = pipelines_project_merge_request_path(project, todo.target)
-
- expect(page).to have_link "merge request #{todo.target.to_reference}", href: href
+ expect(page).to have_content 'The pipeline failed.'
end
end
@@ -453,15 +444,29 @@ RSpec.describe 'Dashboard Todos' do
it 'has todo present' do
expect(page).to have_selector('.todos-list .todo', count: 1)
end
+ end
- it 'has a link that will take me to the design page' do
- click_link "design #{target.to_reference}"
+ context 'User has a todo for an access requested raised for group membership' do
+ let_it_be(:group) { create(:group, :public) }
- expectation = Gitlab::Routing.url_helpers.designs_project_issue_path(
- target.project, target.issue, target.filename
- )
+ let_it_be(:todo) do
+ create(:todo, :member_access_requested,
+ user: user,
+ target: group,
+ author: author,
+ group: group)
+ end
+
+ before do
+ group.add_owner(user)
+ sign_in(user)
- expect(page).to have_current_path(expectation, ignore_query: true)
+ visit dashboard_todos_path
+ end
+
+ it 'has todo present with access request content' do
+ expect(page).to have_selector('.todos-list .todo', count: 1)
+ expect(page).to have_content "#{author.name} has requested access to group #{group.name}"
end
end
end
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index e25da5854ab..1168a6827fd 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard > User filters projects' do
+RSpec.describe 'Dashboard > User filters projects', feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace, created_at: 2.seconds.ago, updated_at: 2.seconds.ago) }
let(:user2) { create(:user) }
@@ -16,7 +16,6 @@ RSpec.describe 'Dashboard > User filters projects' do
describe 'filtering personal projects' do
before do
- stub_feature_flags(project_list_filter_bar: false)
project2.add_developer(user)
visit dashboard_projects_path
@@ -33,7 +32,6 @@ RSpec.describe 'Dashboard > User filters projects' do
describe 'filtering starred projects', :js do
before do
- stub_feature_flags(project_list_filter_bar: false)
user.toggle_star(project)
visit dashboard_projects_path
@@ -49,8 +47,6 @@ RSpec.describe 'Dashboard > User filters projects' do
describe 'without search bar', :js do
before do
- stub_feature_flags(project_list_filter_bar: false)
-
project2.add_developer(user)
visit dashboard_projects_path
end
@@ -65,175 +61,4 @@ RSpec.describe 'Dashboard > User filters projects' do
expect(page).not_to have_content 'Treasure'
end
end
-
- describe 'with search bar', :js do
- before do
- stub_feature_flags(project_list_filter_bar: true)
-
- project2.add_developer(user)
- visit dashboard_projects_path
- end
-
- # TODO: move these helpers somewhere more useful
- def click_sort_direction
- page.find('.filtered-search-block #filtered-search-sorting-dropdown .reverse-sort-btn').click
- end
-
- def select_dropdown_option(selector, label, option_selector = '.dropdown-menu a')
- dropdown = page.find(selector)
- dropdown.click
-
- dropdown.find(option_selector, text: label, match: :first).click
- end
-
- def expect_to_see_projects(sorted_projects)
- list = page.all('.projects-list .project-name').map(&:text)
- expect(list).to match(sorted_projects)
- end
-
- describe 'Search' do
- it 'executes when the search button is clicked' do
- expect(page).to have_content 'Victorialand'
- expect(page).to have_content 'Treasure'
-
- fill_in 'project-filter-form-field', with: 'Lord vegeta\n'
- find('.filtered-search .btn').click
-
- expect(page).not_to have_content 'Victorialand'
- expect(page).not_to have_content 'Treasure'
- end
-
- it 'will execute when i press enter' do
- expect(page).to have_content 'Victorialand'
- expect(page).to have_content 'Treasure'
-
- fill_in 'project-filter-form-field', with: 'Lord frieza\n'
- find('#project-filter-form-field').native.send_keys :enter
-
- expect(page).not_to have_content 'Victorialand'
- expect(page).not_to have_content 'Treasure'
- end
- end
-
- describe 'Filter' do
- before do
- private_project = create(:project, :private, name: 'Private project', namespace: user.namespace)
- internal_project = create(:project, :internal, name: 'Internal project', namespace: user.namespace)
-
- private_project.add_maintainer(user)
- internal_project.add_maintainer(user)
- end
-
- it 'filters private projects only' do
- select_dropdown_option '#filtered-search-visibility-dropdown > .dropdown', 'Private', '.dropdown-item'
-
- expect(current_url).to match(/visibility_level=0/)
-
- list = page.all('.projects-list .project-name').map(&:text)
-
- expect(list).to contain_exactly("Private project", "Treasure", "Victorialand")
- end
-
- it 'filters internal projects only' do
- select_dropdown_option '#filtered-search-visibility-dropdown > .dropdown', 'Internal', '.dropdown-item'
-
- expect(current_url).to match(/visibility_level=10/)
-
- list = page.all('.projects-list .project-name').map(&:text)
-
- expect(list).to contain_exactly('Internal project')
- end
-
- it 'filters any project' do
- # Selecting the same option in the `GlListbox` does not emit `select` event
- # and that is why URL update won't be triggered. Given that `Any` is a default option
- # we need to explicitly switch from some other option (e.g. `Internal`) to `Any`
- # to trigger the page update
- select_dropdown_option '#filtered-search-visibility-dropdown > .dropdown', 'Internal', '.dropdown-item'
-
- select_dropdown_option '#filtered-search-visibility-dropdown > .dropdown', 'Any', '.dropdown-item'
-
- list = page.all('.projects-list .project-name').map(&:text)
-
- expect(list).to contain_exactly("Internal project", "Private project", "Treasure", "Victorialand")
- end
- end
-
- describe 'Sorting' do
- let(:desc_sorted_project_names) { %w[Treasure Victorialand] }
-
- before do
- user.toggle_star(project)
- user.toggle_star(project2)
- user2.toggle_star(project2)
- end
-
- it 'has all sorting options', :js do
- sorting_dropdown = page.find('.filtered-search-block #filtered-search-sorting-dropdown')
-
- expect(sorting_dropdown).to have_css '.reverse-sort-btn'
-
- sorting_dropdown.click
-
- ['Updated date', 'Created date', 'Name', 'Stars'].each do |label|
- expect(sorting_dropdown).to have_content(label)
- end
- end
-
- it 'defaults to "Name"', :js do
- page.find('.filtered-search-block #filtered-search-sorting-dropdown').click
- active_sorting_option = page.first('.filtered-search-block #filtered-search-sorting-dropdown .is-active')
-
- expect(active_sorting_option).to have_content 'Name'
- end
-
- context 'Sorting by name' do
- it 'sorts the project list' do
- select_dropdown_option '#filtered-search-sorting-dropdown', 'Name'
-
- expect_to_see_projects(desc_sorted_project_names)
-
- click_sort_direction
-
- expect_to_see_projects(desc_sorted_project_names.reverse)
- end
- end
-
- context 'Sorting by Updated date' do
- it 'sorts the project list' do
- select_dropdown_option '#filtered-search-sorting-dropdown', 'Updated date'
-
- expect_to_see_projects(desc_sorted_project_names)
-
- click_sort_direction
-
- expect_to_see_projects(desc_sorted_project_names.reverse)
- end
- end
-
- context 'Sorting by Created date' do
- it 'sorts the project list' do
- select_dropdown_option '#filtered-search-sorting-dropdown', 'Created date'
-
- expect_to_see_projects(desc_sorted_project_names)
-
- click_sort_direction
-
- expect_to_see_projects(desc_sorted_project_names.reverse)
- end
- end
-
- context 'Sorting by Stars' do
- it 'sorts the project list' do
- select_dropdown_option '#filtered-search-sorting-dropdown', 'Stars'
-
- expect_to_see_projects(desc_sorted_project_names)
-
- click_sort_direction
-
- expect_to_see_projects(desc_sorted_project_names.reverse)
- end
- end
- end
- end
end
diff --git a/spec/features/discussion_comments/commit_spec.rb b/spec/features/discussion_comments/commit_spec.rb
index 261e9fb9f3b..5407542dfc6 100644
--- a/spec/features/discussion_comments/commit_spec.rb
+++ b/spec/features/discussion_comments/commit_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Thread Comments Commit', :js do
+RSpec.describe 'Thread Comments Commit', :js, feature_category: :source_code_management do
include RepoHelpers
let(:user) { create(:user) }
diff --git a/spec/features/discussion_comments/issue_spec.rb b/spec/features/discussion_comments/issue_spec.rb
index ebb57b37918..90be3f0760d 100644
--- a/spec/features/discussion_comments/issue_spec.rb
+++ b/spec/features/discussion_comments/issue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Thread Comments Issue', :js do
+RSpec.describe 'Thread Comments Issue', :js, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/features/discussion_comments/merge_request_spec.rb b/spec/features/discussion_comments/merge_request_spec.rb
index a90ff3721d3..64395a44e57 100644
--- a/spec/features/discussion_comments/merge_request_spec.rb
+++ b/spec/features/discussion_comments/merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Thread Comments Merge Request', :js do
+RSpec.describe 'Thread Comments Merge Request', :js, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb
index ca0a6d6e1c5..a703c880737 100644
--- a/spec/features/discussion_comments/snippets_spec.rb
+++ b/spec/features/discussion_comments/snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Thread Comments Snippet', :js do
+RSpec.describe 'Thread Comments Snippet', :js, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
before do
diff --git a/spec/features/display_system_header_and_footer_bar_spec.rb b/spec/features/display_system_header_and_footer_bar_spec.rb
index 0979371a574..22fd0987418 100644
--- a/spec/features/display_system_header_and_footer_bar_spec.rb
+++ b/spec/features/display_system_header_and_footer_bar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Display system header and footer bar' do
+RSpec.describe 'Display system header and footer bar', feature_category: :not_owned do
let(:header_message) { "Foo" }
let(:footer_message) { "Bar" }
diff --git a/spec/features/error_pages_spec.rb b/spec/features/error_pages_spec.rb
index 8dc9e5ade46..6a322fd53d4 100644
--- a/spec/features/error_pages_spec.rb
+++ b/spec/features/error_pages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Error Pages', :js do
+RSpec.describe 'Error Pages', :js, feature_category: :error_tracking do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/error_tracking/user_filters_errors_by_status_spec.rb b/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
index 2ac43f67f64..168c4f330ca 100644
--- a/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
+++ b/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'When a user filters Sentry errors by status', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do
+RSpec.describe 'When a user filters Sentry errors by status', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline,
+feature_category: :error_tracking do
include_context 'sentry error tracking context feature'
let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') }
diff --git a/spec/features/error_tracking/user_searches_sentry_errors_spec.rb b/spec/features/error_tracking/user_searches_sentry_errors_spec.rb
index 40718deed75..6026b42f7de 100644
--- a/spec/features/error_tracking/user_searches_sentry_errors_spec.rb
+++ b/spec/features/error_tracking/user_searches_sentry_errors_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'When a user searches for Sentry errors', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do
+RSpec.describe 'When a user searches for Sentry errors', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline,
+feature_category: :error_tracking do
include_context 'sentry error tracking context feature'
let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') }
diff --git a/spec/features/error_tracking/user_sees_error_details_spec.rb b/spec/features/error_tracking/user_sees_error_details_spec.rb
index ecbb3fe0412..d7676d90d21 100644
--- a/spec/features/error_tracking/user_sees_error_details_spec.rb
+++ b/spec/features/error_tracking/user_sees_error_details_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'View error details page', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do
+RSpec.describe 'View error details page', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline,
+feature_category: :error_tracking do
include_context 'sentry error tracking context feature'
context 'with current user as project owner' do
diff --git a/spec/features/error_tracking/user_sees_error_index_spec.rb b/spec/features/error_tracking/user_sees_error_index_spec.rb
index 21f9e688e3f..b7dfb6afc18 100644
--- a/spec/features/error_tracking/user_sees_error_index_spec.rb
+++ b/spec/features/error_tracking/user_sees_error_index_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'View error index page', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do
+RSpec.describe 'View error index page', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline,
+feature_category: :error_tracking do
include_context 'sentry error tracking context feature'
let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') }
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index c3096677a73..1f09b01ddec 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Expand and collapse diffs', :js do
+RSpec.describe 'Expand and collapse diffs', :js, feature_category: :source_code_management do
let(:branch) { 'expand-collapse-diffs' }
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb
index ba09cc20154..3ffa0dc5b64 100644
--- a/spec/features/explore/groups_list_spec.rb
+++ b/spec/features/explore/groups_list_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Explore Groups page', :js do
+RSpec.describe 'Explore Groups page', :js, feature_category: :subgroups do
let!(:user) { create :user }
let!(:group) { create(:group) }
let!(:public_group) { create(:group, :public) }
diff --git a/spec/features/explore/groups_spec.rb b/spec/features/explore/groups_spec.rb
index 201dc24b359..458f83dffb4 100644
--- a/spec/features/explore/groups_spec.rb
+++ b/spec/features/explore/groups_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Explore Groups', :js do
+RSpec.describe 'Explore Groups', :js, feature_category: :subgroups do
let(:user) { create :user }
let(:group) { create :group }
let!(:private_project) do
diff --git a/spec/features/explore/topics_spec.rb b/spec/features/explore/topics_spec.rb
index f0c57c2417a..b5787a2dba8 100644
--- a/spec/features/explore/topics_spec.rb
+++ b/spec/features/explore/topics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Explore Topics' do
+RSpec.describe 'Explore Topics', feature_category: :users do
context 'when no topics exist' do
it 'renders empty message', :aggregate_failures do
visit topics_explore_projects_path
diff --git a/spec/features/explore/user_explores_projects_spec.rb b/spec/features/explore/user_explores_projects_spec.rb
index c082ff1fb0c..f54a51c9ac9 100644
--- a/spec/features/explore/user_explores_projects_spec.rb
+++ b/spec/features/explore/user_explores_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User explores projects' do
+RSpec.describe 'User explores projects', feature_category: :users do
context 'when some projects exist' do
let_it_be(:archived_project) { create(:project, :archived) }
let_it_be(:internal_project) { create(:project, :internal) }
@@ -35,8 +35,6 @@ RSpec.describe 'User explores projects' do
before do
sign_in(user)
-
- stub_feature_flags(project_list_filter_bar: false)
end
shared_examples 'empty search results' do
diff --git a/spec/features/file_uploads/attachment_spec.rb b/spec/features/file_uploads/attachment_spec.rb
index 41da0e9fbe0..cff0c0b52b4 100644
--- a/spec/features/file_uploads/attachment_spec.rb
+++ b/spec/features/file_uploads/attachment_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Upload an attachment', :api, :js do
+RSpec.describe 'Upload an attachment', :api, :js, feature_category: :projects do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
diff --git a/spec/features/file_uploads/ci_artifact_spec.rb b/spec/features/file_uploads/ci_artifact_spec.rb
index 4f3b6c90ad4..420329cc952 100644
--- a/spec/features/file_uploads/ci_artifact_spec.rb
+++ b/spec/features/file_uploads/ci_artifact_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Upload ci artifact', :api, :js do
+RSpec.describe 'Upload ci artifact', :api, :js, feature_category: :build_artifacts do
include_context 'file upload requests helpers'
let_it_be(:user) { create(:user, :admin) }
diff --git a/spec/features/file_uploads/git_lfs_spec.rb b/spec/features/file_uploads/git_lfs_spec.rb
index 8d15c5c33f7..6af4bef2ec4 100644
--- a/spec/features/file_uploads/git_lfs_spec.rb
+++ b/spec/features/file_uploads/git_lfs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Upload a git lfs object', :js do
+RSpec.describe 'Upload a git lfs object', :js, feature_category: :source_code_management do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
diff --git a/spec/features/file_uploads/graphql_add_design_spec.rb b/spec/features/file_uploads/graphql_add_design_spec.rb
index 17fbf5f6838..0b61c952b55 100644
--- a/spec/features/file_uploads/graphql_add_design_spec.rb
+++ b/spec/features/file_uploads/graphql_add_design_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Upload a design through graphQL', :js do
+RSpec.describe 'Upload a design through graphQL', :js, feature_category: :design_management do
include_context 'file upload requests helpers'
let_it_be(:query) do
diff --git a/spec/features/file_uploads/group_import_spec.rb b/spec/features/file_uploads/group_import_spec.rb
index a8592f99bd6..f5082e31c06 100644
--- a/spec/features/file_uploads/group_import_spec.rb
+++ b/spec/features/file_uploads/group_import_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Upload a group export archive', :api, :js do
+RSpec.describe 'Upload a group export archive', :api, :js, feature_category: :subgroups do
include_context 'file upload requests helpers'
let_it_be(:user) { create(:user, :admin) }
diff --git a/spec/features/file_uploads/maven_package_spec.rb b/spec/features/file_uploads/maven_package_spec.rb
index 70302142fa2..8a8bac53613 100644
--- a/spec/features/file_uploads/maven_package_spec.rb
+++ b/spec/features/file_uploads/maven_package_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Upload a maven package', :api, :js do
+RSpec.describe 'Upload a maven package', :api, :js, feature_category: :package_registry do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
diff --git a/spec/features/file_uploads/multipart_invalid_uploads_spec.rb b/spec/features/file_uploads/multipart_invalid_uploads_spec.rb
index cff8b4e61a5..c4c5eb6b74b 100644
--- a/spec/features/file_uploads/multipart_invalid_uploads_spec.rb
+++ b/spec/features/file_uploads/multipart_invalid_uploads_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe 'Invalid uploads that must be rejected', :api, :js do
+RSpec.describe 'Invalid uploads that must be rejected', :api, :js, feature_category: :package_registry do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
context 'invalid upload key', :capybara_ignore_server_errors do
diff --git a/spec/features/file_uploads/nuget_package_spec.rb b/spec/features/file_uploads/nuget_package_spec.rb
index cbffd34d4ab..0dc48114e9a 100644
--- a/spec/features/file_uploads/nuget_package_spec.rb
+++ b/spec/features/file_uploads/nuget_package_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Upload a nuget package', :api, :js do
+RSpec.describe 'Upload a nuget package', :api, :js, feature_category: :package_registry do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
diff --git a/spec/features/file_uploads/project_import_spec.rb b/spec/features/file_uploads/project_import_spec.rb
index 82b6f490d2a..c261834206d 100644
--- a/spec/features/file_uploads/project_import_spec.rb
+++ b/spec/features/file_uploads/project_import_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Upload a project export archive', :api, :js do
+RSpec.describe 'Upload a project export archive', :api, :js, feature_category: :projects do
include_context 'file upload requests helpers'
let_it_be(:user) { create(:user, :admin) }
diff --git a/spec/features/file_uploads/rubygem_package_spec.rb b/spec/features/file_uploads/rubygem_package_spec.rb
index f91fb407b28..9c4512e4eab 100644
--- a/spec/features/file_uploads/rubygem_package_spec.rb
+++ b/spec/features/file_uploads/rubygem_package_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Upload a RubyGems package', :api, :js do
+RSpec.describe 'Upload a RubyGems package', :api, :js, feature_category: :package_registry do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
diff --git a/spec/features/file_uploads/user_avatar_spec.rb b/spec/features/file_uploads/user_avatar_spec.rb
index 34cfb4a4128..06501e09866 100644
--- a/spec/features/file_uploads/user_avatar_spec.rb
+++ b/spec/features/file_uploads/user_avatar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Upload a user avatar', :js do
+RSpec.describe 'Upload a user avatar', :js, feature_category: :users do
let_it_be(:user, reload: true) { create(:user) }
let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
diff --git a/spec/features/frequently_visited_projects_and_groups_spec.rb b/spec/features/frequently_visited_projects_and_groups_spec.rb
index 7fbbc4dfc85..50e20910e16 100644
--- a/spec/features/frequently_visited_projects_and_groups_spec.rb
+++ b/spec/features/frequently_visited_projects_and_groups_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Frequently visited items', :js do
+RSpec.describe 'Frequently visited items', :js, feature_category: :not_owned do
include Spec::Support::Helpers::Features::TopNavSpecHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/gitlab_experiments_spec.rb b/spec/features/gitlab_experiments_spec.rb
index af14b6e2e95..c1417f6f7c5 100644
--- a/spec/features/gitlab_experiments_spec.rb
+++ b/spec/features/gitlab_experiments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Gitlab::Experiment", :js do
+RSpec.describe "Gitlab::Experiment", :js, feature_category: :experimentation_activation do
# This is part of a set of tests that ensure that tracking remains
# consistent at the front end layer. Since we don't want to actually
# introduce an experiment in real code, we're going to simulate it
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index 2e63ec2d4f2..7c55551e9c3 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Global search', :js do
+RSpec.describe 'Global search', :js, feature_category: :global_search do
include AfterNextHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/graphiql_spec.rb b/spec/features/graphiql_spec.rb
index 7729cdaa362..34c1797b6ba 100644
--- a/spec/features/graphiql_spec.rb
+++ b/spec/features/graphiql_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'GraphiQL' do
+RSpec.describe 'GraphiQL', feature_category: :integrations do
context 'without relative_url_root' do
before do
visit '/-/graphql-explorer'
diff --git a/spec/features/graphql_known_operations_spec.rb b/spec/features/graphql_known_operations_spec.rb
index 80214307be3..5b2205a4440 100644
--- a/spec/features/graphql_known_operations_spec.rb
+++ b/spec/features/graphql_known_operations_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
# We need to distinguish between known and unknown GraphQL operations. This spec
# tests that we set up Gitlab::Graphql::KnownOperations.default which requires
# integration of FE queries, webpack plugin, and BE.
-RSpec.describe 'Graphql known operations', :js do
+RSpec.describe 'Graphql known operations', :js, feature_category: :integrations do
around do |example|
# Let's make sure we aren't receiving or leaving behind any side-effects
# https://gitlab.com/gitlab-org/gitlab/-/jobs/1743294100
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index e2c659d7dfe..117f50aefc6 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group variables', :js do
+RSpec.describe 'Group variables', :js, feature_category: :pipeline_authoring do
let(:user) { create(:user) }
let(:group) { create(:group) }
let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test_value', masked: true, group: group) }
diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb
index 5bac80959b1..7e592b3f48b 100644
--- a/spec/features/groups/activity_spec.rb
+++ b/spec/features/groups/activity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group activity page' do
+RSpec.describe 'Group activity page', feature_category: :subgroups do
let(:user) { create(:group_member, :developer, user: create(:user), group: group).user }
let(:group) { create(:group) }
let(:path) { activity_group_path(group) }
diff --git a/spec/features/groups/board_sidebar_spec.rb b/spec/features/groups/board_sidebar_spec.rb
index 10ef28f3fbc..8216bc3249d 100644
--- a/spec/features/groups/board_sidebar_spec.rb
+++ b/spec/features/groups/board_sidebar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group Issue Boards', :js do
+RSpec.describe 'Group Issue Boards', :js, feature_category: :subgroups do
include BoardHelpers
let(:group) { create(:group) }
diff --git a/spec/features/groups/board_spec.rb b/spec/features/groups/board_spec.rb
index aece6d790b5..11ec38f637b 100644
--- a/spec/features/groups/board_spec.rb
+++ b/spec/features/groups/board_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group Boards' do
+RSpec.describe 'Group Boards', feature_category: :team_planning do
include DragTo
include MobileHelpers
include BoardHelpers
@@ -35,7 +35,7 @@ RSpec.describe 'Group Boards' do
page.within("[data-testid='project-select-dropdown']") do
find('button.gl-dropdown-toggle').click
- find('.gl-new-dropdown-item button').click
+ find('.gl-dropdown-item button').click
end
click_button 'Create issue'
diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb
index 6b512323d4d..3e565dd8eab 100644
--- a/spec/features/groups/clusters/user_spec.rb
+++ b/spec/features/groups/clusters/user_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User Cluster', :js do
+RSpec.describe 'User Cluster', :js, feature_category: :users do
include GoogleApi::CloudPlatformHelpers
let(:group) { create(:group) }
diff --git a/spec/features/groups/container_registry_spec.rb b/spec/features/groups/container_registry_spec.rb
index 7bef2dc9416..11f94967aaf 100644
--- a/spec/features/groups/container_registry_spec.rb
+++ b/spec/features/groups/container_registry_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Container Registry', :js do
+RSpec.describe 'Container Registry', :js, feature_category: :container_registry do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
@@ -97,8 +97,6 @@ RSpec.describe 'Container Registry', :js do
expect(find('.modal .modal-title')).to have_content _('Remove tag')
find('.modal .modal-footer .btn-danger').click
end
-
- it_behaves_like 'rejecting tags destruction for an importing repository on', tags: ['latest']
end
end
diff --git a/spec/features/groups/crm/contacts/create_spec.rb b/spec/features/groups/crm/contacts/create_spec.rb
index b10b2afe35c..860cadd322d 100644
--- a/spec/features/groups/crm/contacts/create_spec.rb
+++ b/spec/features/groups/crm/contacts/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a CRM contact', :js do
+RSpec.describe 'Create a CRM contact', :js, feature_category: :service_desk do
let(:user) { create(:user) }
let(:group) { create(:group, :crm_enabled) }
let!(:organization) { create(:organization, group: group, name: 'GitLab') }
diff --git a/spec/features/groups/dependency_proxy_for_containers_spec.rb b/spec/features/groups/dependency_proxy_for_containers_spec.rb
index ae721e7b91f..c0456140291 100644
--- a/spec/features/groups/dependency_proxy_for_containers_spec.rb
+++ b/spec/features/groups/dependency_proxy_for_containers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group Dependency Proxy for containers', :js do
+RSpec.describe 'Group Dependency Proxy for containers', :js, feature_category: :dependency_proxy do
include DependencyProxyHelpers
include_context 'file upload requests helpers'
diff --git a/spec/features/groups/dependency_proxy_spec.rb b/spec/features/groups/dependency_proxy_spec.rb
index af9c4a40729..05984d40ea6 100644
--- a/spec/features/groups/dependency_proxy_spec.rb
+++ b/spec/features/groups/dependency_proxy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group Dependency Proxy' do
+RSpec.describe 'Group Dependency Proxy', feature_category: :dependency_proxy do
let(:owner) { create(:user) }
let(:reporter) { create(:user) }
let(:group) { create(:group) }
diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb
index f1a8f97461a..a37c40f50e0 100644
--- a/spec/features/groups/empty_states_spec.rb
+++ b/spec/features/groups/empty_states_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group empty states' do
+RSpec.describe 'Group empty states', feature_category: :subgroups do
let(:group) { create(:group) }
let(:user) { create(:group_member, :developer, user: create(:user), group: group).user }
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 59a7feb813b..dce5b67d694 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,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'The group page' do
+RSpec.describe 'The group page', feature_category: :subgroups do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
diff --git a/spec/features/groups/group_runners_spec.rb b/spec/features/groups/group_runners_spec.rb
index c9d1c69e9e1..ab53ef7c470 100644
--- a/spec/features/groups/group_runners_spec.rb
+++ b/spec/features/groups/group_runners_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Group Runners" do
+RSpec.describe "Group Runners", feature_category: :runner_fleet do
include Spec::Support::Helpers::Features::RunnersHelpers
include Spec::Support::Helpers::ModalHelpers
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 81ff0088e1e..fe1b0909c06 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Edit group settings' do
+RSpec.describe 'Edit group settings', feature_category: :subgroups do
let(:user) { create(:user) }
let(:group) { create(:group, path: 'foo') }
@@ -147,7 +147,7 @@ RSpec.describe 'Edit group settings' do
selected_group.add_owner(user)
end
- it 'can successfully transfer the group' do
+ it 'can successfully transfer the group', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384966' do
visit edit_group_path(selected_group)
page.within('[data-testid="transfer-locations-dropdown"]') do
diff --git a/spec/features/groups/import_export/connect_instance_spec.rb b/spec/features/groups/import_export/connect_instance_spec.rb
index ae03e64cf59..11cc4bb9b37 100644
--- a/spec/features/groups/import_export/connect_instance_spec.rb
+++ b/spec/features/groups/import_export/connect_instance_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Import/Export - Connect to another instance', :js do
+RSpec.describe 'Import/Export - Connect to another instance', :js, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
@@ -10,74 +10,96 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do
group.add_owner(user)
end
- before do
- gitlab_sign_in(user)
+ context 'when importing group by direct transfer is enabled' do
+ before do
+ stub_application_setting(bulk_import_enabled: true)
- visit new_group_path
+ open_import_group
+ end
- click_link 'Import group'
- end
+ context 'when the user provides valid credentials' do
+ source_url = 'https://gitlab.com'
- context 'when the user provides valid credentials' do
- source_url = 'https://gitlab.com'
+ include_context 'bulk imports requests context', source_url
- include_context 'bulk imports requests context', source_url
+ it 'successfully connects to remote instance' do
+ pat = 'demo-pat'
- it 'successfully connects to remote instance' do
- pat = 'demo-pat'
+ expect(page).to have_content 'Import groups from another instance of GitLab'
+ expect(page).to have_content 'Not all related objects are migrated'
- expect(page).to have_content 'Import groups from another instance of GitLab'
- expect(page).to have_content 'Not all related objects are migrated'
+ fill_in :bulk_import_gitlab_url, with: source_url
+ fill_in :bulk_import_gitlab_access_token, with: pat
- fill_in :bulk_import_gitlab_url, with: source_url
- fill_in :bulk_import_gitlab_access_token, with: pat
+ click_on 'Connect instance'
- click_on 'Connect instance'
+ expect(page).to have_content 'Showing 1-1 of 42 groups that you own from %{url}' % { url: source_url }
+ expect(page).to have_content 'stub-group'
- expect(page).to have_content 'Showing 1-1 of 42 groups that you own from %{url}' % { url: source_url }
- expect(page).to have_content 'stub-group'
+ visit '/'
- visit '/'
+ wait_for_all_requests
+ end
+ end
+
+ context 'when the user provides invalid url' do
+ it 'reports an error' do
+ source_url = 'invalid-url'
+ pat = 'demo-pat'
+
+ fill_in :bulk_import_gitlab_url, with: source_url
+ fill_in :bulk_import_gitlab_access_token, with: pat
- wait_for_all_requests
+ click_on 'Connect instance'
+
+ expect(page).to have_content 'Specified URL cannot be used'
+ end
end
- end
- context 'when the user provides invalid url' do
- it 'reports an error' do
- source_url = 'invalid-url'
- pat = 'demo-pat'
+ context 'when the user does not fill in source URL' do
+ it 'reports an error' do
+ pat = 'demo-pat'
- fill_in :bulk_import_gitlab_url, with: source_url
- fill_in :bulk_import_gitlab_access_token, with: pat
+ fill_in :bulk_import_gitlab_access_token, with: pat
- click_on 'Connect instance'
+ click_on 'Connect instance'
- expect(page).to have_content 'Specified URL cannot be used'
+ expect(page).to have_content 'Please fill in GitLab source URL'
+ end
end
- end
- context 'when the user does not fill in source URL' do
- it 'reports an error' do
- pat = 'demo-pat'
+ context 'when the user does not fill in access token' do
+ it 'reports an error' do
+ source_url = 'https://gitlab.com'
- fill_in :bulk_import_gitlab_access_token, with: pat
+ fill_in :bulk_import_gitlab_url, with: source_url
- click_on 'Connect instance'
+ click_on 'Connect instance'
- expect(page).to have_content 'Please fill in GitLab source URL'
+ expect(page).to have_content 'Please fill in your personal access token'
+ end
end
end
- context 'when the user does not fill in access token' do
- it 'reports an error' do
- source_url = 'https://gitlab.com'
-
- fill_in :bulk_import_gitlab_url, with: source_url
+ context 'when importing group by direct transfer is disabled' do
+ before do
+ stub_application_setting(bulk_import_enabled: false)
- click_on 'Connect instance'
+ open_import_group
+ end
- expect(page).to have_content 'Please fill in your personal access token'
+ it 'renders fields and button disabled' do
+ expect(page).to have_field('GitLab source URL', disabled: true)
+ expect(page).to have_field('Personal access token', disabled: true)
+ expect(page).to have_button('Connect instance', disabled: true)
end
end
+
+ def open_import_group
+ gitlab_sign_in(user)
+
+ visit new_group_path
+
+ click_link 'Import group'
+ end
end
diff --git a/spec/features/groups/import_export/export_file_spec.rb b/spec/features/groups/import_export/export_file_spec.rb
index e3cb1ad77a7..885cfa0f595 100644
--- a/spec/features/groups/import_export/export_file_spec.rb
+++ b/spec/features/groups/import_export/export_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group Export', :js do
+RSpec.describe 'Group Export', :js, feature_category: :importers do
include ExportFileHelper
let_it_be(:user) { create(:user) }
diff --git a/spec/features/groups/import_export/import_file_spec.rb b/spec/features/groups/import_export/import_file_spec.rb
index b69b8bf2c19..f66062b9ac3 100644
--- a/spec/features/groups/import_export/import_file_spec.rb
+++ b/spec/features/groups/import_export/import_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Import/Export - Group Import', :js do
+RSpec.describe 'Import/Export - Group Import', :js, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:import_path) { "#{Dir.tmpdir}/group_import_spec" }
diff --git a/spec/features/groups/import_export/migration_history_spec.rb b/spec/features/groups/import_export/migration_history_spec.rb
index 243bdcc13a9..f851c5e2ec5 100644
--- a/spec/features/groups/import_export/migration_history_spec.rb
+++ b/spec/features/groups/import_export/migration_history_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Import/Export - GitLab migration history', :js do
+RSpec.describe 'Import/Export - GitLab migration history', :js, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:user_import_1) { create(:bulk_import, user: user) }
diff --git a/spec/features/groups/integrations/user_activates_mattermost_slash_command_spec.rb b/spec/features/groups/integrations/user_activates_mattermost_slash_command_spec.rb
index 02aa418cd73..fbdd760f7fb 100644
--- a/spec/features/groups/integrations/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/groups/integrations/user_activates_mattermost_slash_command_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates the group-level Mattermost Slash Command integration', :js do
+RSpec.describe 'User activates the group-level Mattermost Slash Command integration', :js, feature_category: :build do
include_context 'group integration activation'
before do
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index d4e88505118..00c0d4c3ebe 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group issues page' do
+RSpec.describe 'Group issues page', feature_category: :subgroups do
include FilteredSearchHelpers
include DragTo
@@ -11,6 +11,10 @@ RSpec.describe 'Group issues page' do
let(:project_with_issues_disabled) { create(:project, :issues_disabled, group: group) }
let(:path) { issues_group_path(group) }
+ before do
+ stub_feature_flags(or_issuable_queries: false)
+ end
+
context 'with shared examples', :js do
let(:issuable) { create(:issue, project: project, title: "this is my created issuable") }
diff --git a/spec/features/groups/labels/create_spec.rb b/spec/features/groups/labels/create_spec.rb
index 19433e612ff..5b57e670c1d 100644
--- a/spec/features/groups/labels/create_spec.rb
+++ b/spec/features/groups/labels/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a group label' do
+RSpec.describe 'Create a group label', feature_category: :team_planning do
let(:user) { create(:user) }
let(:group) { create(:group) }
diff --git a/spec/features/groups/labels/edit_spec.rb b/spec/features/groups/labels/edit_spec.rb
index cf1729af97d..2cbe44e11bf 100644
--- a/spec/features/groups/labels/edit_spec.rb
+++ b/spec/features/groups/labels/edit_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Edit group label' do
+RSpec.describe 'Edit group label', feature_category: :team_planning do
include Spec::Support::Helpers::ModalHelpers
let(:user) { create(:user) }
diff --git a/spec/features/groups/labels/index_spec.rb b/spec/features/groups/labels/index_spec.rb
index 68f03368989..ea27fa2c5d9 100644
--- a/spec/features/groups/labels/index_spec.rb
+++ b/spec/features/groups/labels/index_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group labels' do
+RSpec.describe 'Group labels', feature_category: :team_planning do
let(:user) { create(:user) }
let(:group) { create(:group) }
let!(:label) { create(:group_label, group: group) }
diff --git a/spec/features/groups/labels/search_labels_spec.rb b/spec/features/groups/labels/search_labels_spec.rb
index fbb0acfb923..478d35951f9 100644
--- a/spec/features/groups/labels/search_labels_spec.rb
+++ b/spec/features/groups/labels/search_labels_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Search for labels', :js do
+RSpec.describe 'Search for labels', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:group) { create(:group) }
let!(:label1) { create(:group_label, title: 'Foo', description: 'Lorem ipsum', group: group) }
diff --git a/spec/features/groups/labels/sort_labels_spec.rb b/spec/features/groups/labels/sort_labels_spec.rb
index 9d05703aae6..c2410246fe1 100644
--- a/spec/features/groups/labels/sort_labels_spec.rb
+++ b/spec/features/groups/labels/sort_labels_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Sort labels', :js do
+RSpec.describe 'Sort labels', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:group) { create(:group) }
let!(:label1) { create(:group_label, title: 'Foo', description: 'Lorem ipsum', group: group) }
diff --git a/spec/features/groups/labels/subscription_spec.rb b/spec/features/groups/labels/subscription_spec.rb
index 231c4b33bee..4d391074e62 100644
--- a/spec/features/groups/labels/subscription_spec.rb
+++ b/spec/features/groups/labels/subscription_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Labels subscription' do
+RSpec.describe 'Labels subscription', feature_category: :team_planning do
let(:user) { create(:user) }
let(:group) { create(:group) }
let!(:label1) { create(:group_label, group: group, title: 'foo') }
diff --git a/spec/features/groups/labels/user_sees_links_to_issuables_spec.rb b/spec/features/groups/labels/user_sees_links_to_issuables_spec.rb
index b0508633065..4caf5ba5314 100644
--- a/spec/features/groups/labels/user_sees_links_to_issuables_spec.rb
+++ b/spec/features/groups/labels/user_sees_links_to_issuables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > Labels > User sees links to issuables' do
+RSpec.describe 'Groups > Labels > User sees links to issuables', feature_category: :team_planning do
let_it_be(:group) { create(:group, :public) }
before do
diff --git a/spec/features/groups/members/filter_members_spec.rb b/spec/features/groups/members/filter_members_spec.rb
index 917b35659a6..dc33bb11bea 100644
--- a/spec/features/groups/members/filter_members_spec.rb
+++ b/spec/features/groups/members/filter_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > Members > Filter members', :js do
+RSpec.describe 'Groups > Members > Filter members', :js, feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
let(:user) { create(:user) }
diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb
index 66f251c859a..cfb1b24bccb 100644
--- a/spec/features/groups/members/leave_group_spec.rb
+++ b/spec/features/groups/members/leave_group_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > Members > Leave group' do
+RSpec.describe 'Groups > Members > Leave group', feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::ModalHelpers
diff --git a/spec/features/groups/members/list_members_spec.rb b/spec/features/groups/members/list_members_spec.rb
index b81949da85d..1aea5a76b41 100644
--- a/spec/features/groups/members/list_members_spec.rb
+++ b/spec/features/groups/members/list_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > Members > List members', :js do
+RSpec.describe 'Groups > Members > List members', :js, feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
let(:user1) { create(:user, name: 'John Doe') }
diff --git a/spec/features/groups/members/manage_groups_spec.rb b/spec/features/groups/members/manage_groups_spec.rb
index e4252e2f3aa..ee8786a2e36 100644
--- a/spec/features/groups/members/manage_groups_spec.rb
+++ b/spec/features/groups/members/manage_groups_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > Members > Manage groups', :js do
+RSpec.describe 'Groups > Members > Manage groups', :js, feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
include Spec::Support::Helpers::ModalHelpers
diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb
index 5f28afc23f1..4211f2b6265 100644
--- a/spec/features/groups/members/manage_members_spec.rb
+++ b/spec/features/groups/members/manage_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > Members > Manage members' do
+RSpec.describe 'Groups > Members > Manage members', feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
include Spec::Support::Helpers::ModalHelpers
@@ -72,7 +72,7 @@ RSpec.describe 'Groups > Members > Manage members' do
visit group_group_members_path(group)
- invite_member(user1.name, role: 'Reporter', refresh: false)
+ invite_member(user1.name, role: 'Reporter')
invite_modal = page.find(invite_modal_selector)
expect(invite_modal).to have_content("not authorized to update member")
diff --git a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
index 86185b8dd32..e9f80b05fa7 100644
--- a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > Members > Owner adds member with expiration date', :js do
+RSpec.describe 'Groups > Members > Owner adds member with expiration date', :js, feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
diff --git a/spec/features/groups/members/master_manages_access_requests_spec.rb b/spec/features/groups/members/master_manages_access_requests_spec.rb
index 2a17e7d2a5c..951dc59feca 100644
--- a/spec/features/groups/members/master_manages_access_requests_spec.rb
+++ b/spec/features/groups/members/master_manages_access_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > Members > Maintainer manages access requests' do
+RSpec.describe 'Groups > Members > Maintainer manages access requests', feature_category: :subgroups do
it_behaves_like 'Maintainer manages access requests' do
let(:entity) { create(:group, :public) }
let(:members_page_path) { group_group_members_path(entity) }
diff --git a/spec/features/groups/members/request_access_spec.rb b/spec/features/groups/members/request_access_spec.rb
index f806c7d3704..35eb085a195 100644
--- a/spec/features/groups/members/request_access_spec.rb
+++ b/spec/features/groups/members/request_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > Members > Request access' do
+RSpec.describe 'Groups > Members > Request access', feature_category: :subgroups do
let(:user) { create(:user) }
let(:owner) { create(:user) }
let(:group) { create(:group, :public) }
diff --git a/spec/features/groups/members/search_members_spec.rb b/spec/features/groups/members/search_members_spec.rb
index fe5fed307d7..6b2896b194c 100644
--- a/spec/features/groups/members/search_members_spec.rb
+++ b/spec/features/groups/members/search_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Search group member', :js do
+RSpec.describe 'Search group member', :js, feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
let(:user) { create :user }
diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb
index bf8e64fa1e2..4e9adda5f2b 100644
--- a/spec/features/groups/members/sort_members_spec.rb
+++ b/spec/features/groups/members/sort_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > Members > Sort members', :js do
+RSpec.describe 'Groups > Members > Sort members', :js, feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
let(:owner) { create(:user, name: 'John Doe', created_at: 5.days.ago, last_activity_on: Date.today) }
diff --git a/spec/features/groups/members/tabs_spec.rb b/spec/features/groups/members/tabs_spec.rb
index 2e9f332c0d6..2dc116842b3 100644
--- a/spec/features/groups/members/tabs_spec.rb
+++ b/spec/features/groups/members/tabs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > Members > Tabs', :js do
+RSpec.describe 'Groups > Members > Tabs', :js, feature_category: :subgroups do
using RSpec::Parameterized::TableSyntax
shared_examples 'active "Members" tab' do
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 296b839c8fc..87f1f422e90 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group merge requests page' do
+RSpec.describe 'Group merge requests page', feature_category: :code_review do
include FilteredSearchHelpers
let(:path) { merge_requests_group_path(group) }
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 92a40459737..a70a1e2e70b 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group milestones' do
+RSpec.describe 'Group milestones', feature_category: :subgroups do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project_empty_repo, group: group) }
let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group).user }
diff --git a/spec/features/groups/milestones/gfm_autocomplete_spec.rb b/spec/features/groups/milestones/gfm_autocomplete_spec.rb
index 1fec6091f1e..8df097dde88 100644
--- a/spec/features/groups/milestones/gfm_autocomplete_spec.rb
+++ b/spec/features/groups/milestones/gfm_autocomplete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'GFM autocomplete', :js do
+RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:group) { create(:group, name: 'Ancestor') }
let_it_be(:project) { create(:project, :repository, group: group) }
diff --git a/spec/features/groups/milestones_sorting_spec.rb b/spec/features/groups/milestones_sorting_spec.rb
index 6f3fc72775f..5543938957a 100644
--- a/spec/features/groups/milestones_sorting_spec.rb
+++ b/spec/features/groups/milestones_sorting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Milestones sorting', :js do
+RSpec.describe 'Milestones sorting', :js, feature_category: :team_planning do
let(:group) { create(:group) }
let!(:project) { create(:project_empty_repo, group: group) }
let!(:other_project) { create(:project_empty_repo, group: group) }
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index b3fb563a202..180ccab78bc 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group navbar' do
+RSpec.describe 'Group navbar', feature_category: :navigation do
include NavbarStructureHelper
include WikiHelpers
diff --git a/spec/features/groups/new_group_page_spec.rb b/spec/features/groups/new_group_page_spec.rb
index 6a8af9c31fd..662ef734299 100644
--- a/spec/features/groups/new_group_page_spec.rb
+++ b/spec/features/groups/new_group_page_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'New group page', :js do
+RSpec.describe 'New group page', :js, feature_category: :subgroups do
let(:user) { create(:user) }
let(:group) { create(:group) }
diff --git a/spec/features/groups/packages_spec.rb b/spec/features/groups/packages_spec.rb
index 26338b03349..dd238657fbc 100644
--- a/spec/features/groups/packages_spec.rb
+++ b/spec/features/groups/packages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group Packages' do
+RSpec.describe 'Group Packages', feature_category: :package_registry 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/features/groups/settings/access_tokens_spec.rb b/spec/features/groups/settings/access_tokens_spec.rb
index 198d3a40df2..1bee3be1ddb 100644
--- a/spec/features/groups/settings/access_tokens_spec.rb
+++ b/spec/features/groups/settings/access_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group > Settings > Access Tokens', :js do
+RSpec.describe 'Group > Settings > Access Tokens', :js, feature_category: :authentication_and_authorization do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/groups/settings/ci_cd_spec.rb b/spec/features/groups/settings/ci_cd_spec.rb
index 50c481c115c..a1acb73178b 100644
--- a/spec/features/groups/settings/ci_cd_spec.rb
+++ b/spec/features/groups/settings/ci_cd_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group CI/CD settings' do
+RSpec.describe 'Group CI/CD settings', feature_category: :continuous_integration do
include WaitForRequests
let_it_be(:user) { create(:user) }
@@ -23,11 +23,6 @@ RSpec.describe 'Group CI/CD settings' do
visit group_settings_ci_cd_path(group)
end
- it 'displays the new group runners view banner' do
- expect(page).to have_content(s_('Runners|New group runners view'))
- expect(page).to have_link(href: group_runners_path(group))
- end
-
it 'has "Enable shared runners for this group" toggle', :js do
expect(shared_runners_toggle).to have_content(_('Enable shared runners for this group'))
end
diff --git a/spec/features/groups/settings/group_badges_spec.rb b/spec/features/groups/settings/group_badges_spec.rb
index 5bf736cc7ce..07c8451f8fb 100644
--- a/spec/features/groups/settings/group_badges_spec.rb
+++ b/spec/features/groups/settings/group_badges_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group Badges' do
+RSpec.describe 'Group Badges', feature_category: :subgroups do
include WaitForRequests
let(:user) { create(:user) }
diff --git a/spec/features/groups/settings/manage_applications_spec.rb b/spec/features/groups/settings/manage_applications_spec.rb
index 277471cb304..e7b87cda506 100644
--- a/spec/features/groups/settings/manage_applications_spec.rb
+++ b/spec/features/groups/settings/manage_applications_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User manages applications' do
+RSpec.describe 'User manages applications', feature_category: :subgroups do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:new_application_path) { group_settings_applications_path(group) }
diff --git a/spec/features/groups/settings/packages_and_registries_spec.rb b/spec/features/groups/settings/packages_and_registries_spec.rb
index 7f3f5775559..60aad8452ce 100644
--- a/spec/features/groups/settings/packages_and_registries_spec.rb
+++ b/spec/features/groups/settings/packages_and_registries_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group Package and registry settings' do
+RSpec.describe 'Group Package and registry settings', feature_category: :package_registry do
include WaitForRequests
let(:user) { create(:user) }
diff --git a/spec/features/groups/settings/repository_spec.rb b/spec/features/groups/settings/repository_spec.rb
index cd7dcbdb28d..c65a8268a18 100644
--- a/spec/features/groups/settings/repository_spec.rb
+++ b/spec/features/groups/settings/repository_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group Repository settings', :js do
+RSpec.describe 'Group Repository settings', :js, feature_category: :source_code_management do
include WaitForRequests
let_it_be(:user) { create(:user) }
diff --git a/spec/features/groups/settings/user_searches_in_settings_spec.rb b/spec/features/groups/settings/user_searches_in_settings_spec.rb
index fe0dd7cec9a..374ac236e20 100644
--- a/spec/features/groups/settings/user_searches_in_settings_spec.rb
+++ b/spec/features/groups/settings/user_searches_in_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches group settings', :js do
+RSpec.describe 'User searches group settings', :js, feature_category: :subgroups do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
diff --git a/spec/features/groups/share_lock_spec.rb b/spec/features/groups/share_lock_spec.rb
index d8207899e24..2f5a5e6ba16 100644
--- a/spec/features/groups/share_lock_spec.rb
+++ b/spec/features/groups/share_lock_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group share with group lock' do
+RSpec.describe 'Group share with group lock', feature_category: :subgroups do
let(:root_owner) { create(:user) }
let(:root_group) { create(:group) }
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index d814906a274..c0af6080d0f 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group show page' do
+RSpec.describe 'Group show page', feature_category: :subgroups do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
diff --git a/spec/features/groups/user_browse_projects_group_page_spec.rb b/spec/features/groups/user_browse_projects_group_page_spec.rb
index 73fde7cafe5..38b879bb5b2 100644
--- a/spec/features/groups/user_browse_projects_group_page_spec.rb
+++ b/spec/features/groups/user_browse_projects_group_page_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User browse group projects page' do
+RSpec.describe 'User browse group projects page', feature_category: :subgroups do
let(:user) { create :user }
let(:group) { create :group }
diff --git a/spec/features/groups/user_sees_package_sidebar_spec.rb b/spec/features/groups/user_sees_package_sidebar_spec.rb
index ee216488232..64422f5cca5 100644
--- a/spec/features/groups/user_sees_package_sidebar_spec.rb
+++ b/spec/features/groups/user_sees_package_sidebar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > sidebar' do
+RSpec.describe 'Groups > sidebar', feature_category: :subgroups do
let(:user) { create(:user) }
let(:group) { create(:group) }
diff --git a/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb
index 4e4c0e509b0..e5e30ed1a55 100644
--- a/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb
+++ b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > User sees users dropdowns in issuables list', :js do
+RSpec.describe 'Groups > User sees users dropdowns in issuables list', :js, feature_category: :subgroups do
include FilteredSearchHelpers
let(:group) { create(:group) }
@@ -11,6 +11,7 @@ RSpec.describe 'Groups > User sees users dropdowns in issuables list', :js do
let!(:project) { create(:project, group: group) }
before do
+ stub_feature_flags(or_issuable_queries: false)
group.add_developer(user_in_dropdown)
sign_in(user_in_dropdown)
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 4e02f6f7ca2..8806d1c2219 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group' do
+RSpec.describe 'Group', feature_category: :subgroups do
let(:user) { create(:user) }
before do
@@ -350,10 +350,10 @@ RSpec.describe 'Group' do
visit path
end
- it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="group[name]"]' },
- { form: '.js-general-settings-form', input: '#group_visibility_level_0' },
- { form: '.js-general-permissions-form', input: '#group_request_access_enabled' },
- { form: '.js-general-permissions-form', input: 'input[name="group[two_factor_grace_period]"]' }]
+ it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="group[name]"]', submit: 'button[type="submit"]' },
+ { form: '.js-general-settings-form', input: '#group_visibility_level_0', submit: 'button[type="submit"]' },
+ { form: '.js-general-permissions-form', input: '#group_request_access_enabled', submit: 'button[type="submit"]' },
+ { form: '.js-general-permissions-form', input: 'input[name="group[two_factor_grace_period]"]', submit: 'button[type="submit"]' }]
it 'saves new settings' do
page.within('.gs-general') do
diff --git a/spec/features/help_dropdown_spec.rb b/spec/features/help_dropdown_spec.rb
index a9c014a9408..a5c9221ad26 100644
--- a/spec/features/help_dropdown_spec.rb
+++ b/spec/features/help_dropdown_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Help Dropdown", :js do
+RSpec.describe "Help Dropdown", :js, feature_category: :not_owned do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index eef48d09f32..6c0901d6169 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Help Pages' do
+RSpec.describe 'Help Pages', feature_category: :not_owned do
describe 'Get the main help page' do
before do
allow(File).to receive(:read).and_call_original
diff --git a/spec/features/ics/dashboard_issues_spec.rb b/spec/features/ics/dashboard_issues_spec.rb
index 1d0ea495757..7115bd7dff7 100644
--- a/spec/features/ics/dashboard_issues_spec.rb
+++ b/spec/features/ics/dashboard_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Issues Calendar Feed' do
+RSpec.describe 'Dashboard Issues Calendar Feed', feature_category: :team_planning do
describe 'GET /issues' do
let!(:user) do
user = create(:user, email: 'private1@example.com')
diff --git a/spec/features/ics/group_issues_spec.rb b/spec/features/ics/group_issues_spec.rb
index f29c39ad4ef..70ec156a7b0 100644
--- a/spec/features/ics/group_issues_spec.rb
+++ b/spec/features/ics/group_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group Issues Calendar Feed' do
+RSpec.describe 'Group Issues Calendar Feed', feature_category: :subgroups do
describe 'GET /issues' do
let!(:user) do
user = create(:user, email: 'private1@example.com')
diff --git a/spec/features/ics/project_issues_spec.rb b/spec/features/ics/project_issues_spec.rb
index 771748060bb..4bbd966d72a 100644
--- a/spec/features/ics/project_issues_spec.rb
+++ b/spec/features/ics/project_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Issues Calendar Feed' do
+RSpec.describe 'Project Issues Calendar Feed', feature_category: :projects do
describe 'GET /issues' do
let!(:user) do
user = create(:user, email: 'private1@example.com')
diff --git a/spec/features/ide/clientside_preview_csp_spec.rb b/spec/features/ide/clientside_preview_csp_spec.rb
index 849fdb0a44c..04427a5c294 100644
--- a/spec/features/ide/clientside_preview_csp_spec.rb
+++ b/spec/features/ide/clientside_preview_csp_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'IDE Clientside Preview CSP' do
+RSpec.describe 'IDE Clientside Preview CSP', feature_category: :web_ide do
let_it_be(:user) { create(:user) }
shared_context 'disable feature' do
diff --git a/spec/features/ide/static_object_external_storage_csp_spec.rb b/spec/features/ide/static_object_external_storage_csp_spec.rb
index 421b5db0dbb..701be2626e5 100644
--- a/spec/features/ide/static_object_external_storage_csp_spec.rb
+++ b/spec/features/ide/static_object_external_storage_csp_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Static Object External Storage Content Security Policy' do
+RSpec.describe 'Static Object External Storage Content Security Policy', feature_category: :web_ide do
let_it_be(:user) { create(:user) }
shared_context 'disable feature' do
diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb
index 4ffa5212970..0074b4b1eb0 100644
--- a/spec/features/ide/user_opens_merge_request_spec.rb
+++ b/spec/features/ide/user_opens_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'IDE merge request', :js do
+RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { project.first_owner }
diff --git a/spec/features/ide_spec.rb b/spec/features/ide_spec.rb
index 1f6d34efc0f..2ca8d3f7156 100644
--- a/spec/features/ide_spec.rb
+++ b/spec/features/ide_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'IDE', :js do
+RSpec.describe 'IDE', :js, feature_category: :web_ide do
include WebIdeSpecHelpers
let_it_be(:ide_iframe_selector) { '#ide iframe' }
@@ -46,10 +46,6 @@ RSpec.describe 'IDE', :js do
end
it_behaves_like 'legacy Web IDE'
-
- it 'does not show switch button' do
- expect(page).not_to have_button('Switch to new Web IDE')
- end
end
context 'with vscode feature flag on and use_legacy_web_ide=true' do
@@ -61,19 +57,6 @@ RSpec.describe 'IDE', :js do
end
it_behaves_like 'legacy Web IDE'
-
- describe 'when user switches to new Web IDE' do
- before do
- click_button('Switch to new Web IDE')
-
- # Confirm modal
- page.within('#confirmationModal') do
- click_button('Switch editors')
- end
- end
-
- it_behaves_like 'new Web IDE'
- end
end
describe 'sub-groups' do
diff --git a/spec/features/import/manifest_import_spec.rb b/spec/features/import/manifest_import_spec.rb
index 520cf850da2..bb3eb34637b 100644
--- a/spec/features/import/manifest_import_spec.rb
+++ b/spec/features/import/manifest_import_spec.rb
@@ -2,9 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Import multiple repositories by uploading a manifest file', :js do
- include Select2Helper
-
+RSpec.describe 'Import multiple repositories by uploading a manifest file', :js, feature_category: :importers do
let(:user) { create(:admin) }
let(:group) { create(:group) }
diff --git a/spec/features/incidents/incident_details_spec.rb b/spec/features/incidents/incident_details_spec.rb
index 7c24943eb6f..e1167285464 100644
--- a/spec/features/incidents/incident_details_spec.rb
+++ b/spec/features/incidents/incident_details_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Incident details', :js do
+RSpec.describe 'Incident details', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:incident) { create(:incident, project: project, author: developer, description: 'description') }
diff --git a/spec/features/incidents/incident_timeline_events_spec.rb b/spec/features/incidents/incident_timeline_events_spec.rb
index ef0eb27d310..3a73ea50247 100644
--- a/spec/features/incidents/incident_timeline_events_spec.rb
+++ b/spec/features/incidents/incident_timeline_events_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Incident timeline events', :js do
+RSpec.describe 'Incident timeline events', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:incident) { create(:incident, project: project) }
@@ -50,9 +50,9 @@ RSpec.describe 'Incident timeline events', :js do
it 'shows the confirmation modal and edits the event' do
click_button _('More actions')
- page.within '.gl-new-dropdown-contents' do
+ page.within '.gl-dropdown-contents' do
expect(page).to have_content(_('Edit'))
- page.find('.gl-new-dropdown-item-text-primary', text: _('Edit')).click
+ page.find('.gl-dropdown-item-text-primary', text: _('Edit')).click
end
expect(page).to have_selector('.common-note-form')
@@ -82,9 +82,9 @@ RSpec.describe 'Incident timeline events', :js do
it 'shows the confirmation modal and deletes the event' do
click_button _('More actions')
- page.within '.gl-new-dropdown-contents' do
+ page.within '.gl-dropdown-contents' do
expect(page).to have_content(_('Delete'))
- page.find('.gl-new-dropdown-item-text-primary', text: 'Delete').click
+ page.find('.gl-dropdown-item-text-primary', text: 'Delete').click
end
page.within '.modal' do
diff --git a/spec/features/incidents/incidents_list_spec.rb b/spec/features/incidents/incidents_list_spec.rb
index 3241e71f537..61983f9290c 100644
--- a/spec/features/incidents/incidents_list_spec.rb
+++ b/spec/features/incidents/incidents_list_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Incident Management index', :js do
+RSpec.describe 'Incident Management index', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:incident) { create(:incident, project: project) }
diff --git a/spec/features/incidents/user_creates_new_incident_spec.rb b/spec/features/incidents/user_creates_new_incident_spec.rb
index 685f6ab791a..bf5d7dac587 100644
--- a/spec/features/incidents/user_creates_new_incident_spec.rb
+++ b/spec/features/incidents/user_creates_new_incident_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Incident Management index', :js do
+RSpec.describe 'Incident Management index', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) }
diff --git a/spec/features/incidents/user_filters_incidents_by_status_spec.rb b/spec/features/incidents/user_filters_incidents_by_status_spec.rb
index 661c737141b..f1f9dd45ff7 100644
--- a/spec/features/incidents/user_filters_incidents_by_status_spec.rb
+++ b/spec/features/incidents/user_filters_incidents_by_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User filters Incident Management table by status', :js do
+RSpec.describe 'User filters Incident Management table by status', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
diff --git a/spec/features/incidents/user_searches_incidents_spec.rb b/spec/features/incidents/user_searches_incidents_spec.rb
index b8e3ff534c3..31a485801c4 100644
--- a/spec/features/incidents/user_searches_incidents_spec.rb
+++ b/spec/features/incidents/user_searches_incidents_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches Incident Management incidents', :js do
+RSpec.describe 'User searches Incident Management incidents', :js, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:incident) { create(:incident, project: project) }
diff --git a/spec/features/incidents/user_uses_quick_actions_spec.rb b/spec/features/incidents/user_uses_quick_actions_spec.rb
index fce9eadd42f..3740f2fca47 100644
--- a/spec/features/incidents/user_uses_quick_actions_spec.rb
+++ b/spec/features/incidents/user_uses_quick_actions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Incidents > User uses quick actions', :js do
+RSpec.describe 'Incidents > User uses quick actions', :js, feature_category: :incident_management do
include Spec::Support::Helpers::Features::NotesHelpers
describe 'incident-only commands' do
diff --git a/spec/features/incidents/user_views_incident_spec.rb b/spec/features/incidents/user_views_incident_spec.rb
index 054a084ea9c..8216aca787a 100644
--- a/spec/features/incidents/user_views_incident_spec.rb
+++ b/spec/features/incidents/user_views_incident_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User views incident" do
+RSpec.describe "User views incident", feature_category: :incident_management do
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:guest) { create(:user) }
let_it_be(:developer) { create(:user) }
@@ -57,7 +57,7 @@ RSpec.describe "User views incident" do
it 'shows incident actions', :js do
click_button 'Incident actions'
- expect(page).to have_link 'Report abuse'
+ expect(page).to have_link 'Report abuse to administrator'
end
end
end
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index 34990a53b51..1091bea1ce3 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group or Project invitations', :aggregate_failures do
+RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_category: :experimentation_expansion do
let_it_be(:owner) { create(:user, name: 'John Doe') }
let_it_be(:group) { create(:group, name: 'Owned') }
let_it_be(:project) { create(:project, :repository, namespace: group) }
@@ -155,11 +155,10 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
let(:new_user) { build_stubbed(:user) }
let(:invite_email) { new_user.email }
let(:group_invite) { create(:group_member, :invited, group: group, invite_email: invite_email, created_by: owner) }
- let(:send_email_confirmation) { true }
let(:extra_params) { { invite_type: Emails::Members::INITIAL_INVITE } }
before do
- stub_application_setting(send_user_confirmation_email: send_email_confirmation)
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
end
context 'when registering using invitation email' do
@@ -181,7 +180,9 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
end
context 'email confirmation disabled' do
- let(:send_email_confirmation) { false }
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'off')
+ end
context 'the user signs up for an account with the invitation email address' do
it 'redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do
@@ -213,6 +214,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
expect { fill_in_sign_up_form(new_user) }.not_to change { User.count }
expect(page).to have_content('prohibited this user from being saved')
expect(page).to have_current_path(user_registration_path, ignore_query: true)
+ expect(find_field('Email').value).to eq(group_invite.invite_email)
end
end
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index a1e80586c05..350b0582565 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'issuable list', :js do
+RSpec.describe 'issuable list', :js, feature_category: :team_planning do
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -26,9 +26,9 @@ RSpec.describe 'issuable list', :js do
it "counts upvotes, downvotes and notes count for each #{issuable_type.to_s.humanize}" do
visit_issuable_list(issuable_type)
- expect(first('.issuable-upvotes')).to have_content(1)
- expect(first('.issuable-downvotes')).to have_content(1)
- expect(first('.issuable-comments')).to have_content(2)
+ expect(first('[data-testid="issuable-upvotes"]')).to have_content(1)
+ expect(first('[data-testid="issuable-downvotes"]')).to have_content(1)
+ expect(first('[data-testid="issuable-comments"]')).to have_content(2)
end
it 'sorts labels alphabetically' do
diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb
index c5a8e9f367c..aeae76b1b77 100644
--- a/spec/features/issuables/markdown_references/internal_references_spec.rb
+++ b/spec/features/issuables/markdown_references/internal_references_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Internal references", :js do
+RSpec.describe "Internal references", :js, feature_category: :team_planning do
include Spec::Support::Helpers::Features::NotesHelpers
let(:private_project_user) { private_project.first_owner }
diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb
index 66d0022f7e9..52464c6be8b 100644
--- a/spec/features/issuables/markdown_references/jira_spec.rb
+++ b/spec/features/issuables/markdown_references/jira_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Jira", :js do
+RSpec.describe "Jira", :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:actual_project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project) }
diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb
index 528420062dd..0190111b2f0 100644
--- a/spec/features/issuables/shortcuts_issuable_spec.rb
+++ b/spec/features/issuables/shortcuts_issuable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Blob shortcuts', :js do
+RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue) { create(:issue, project: project, author: user) }
@@ -87,7 +87,7 @@ RSpec.describe 'Blob shortcuts', :js do
it "opens milestones dropdown for editing" do
find('body').native.send_key('m')
- expect(find('[data-testid="milestone-edit"]')).to have_selector('.gl-new-dropdown-inner')
+ expect(find('[data-testid="milestone-edit"]')).to have_selector('.gl-dropdown-inner')
end
end
diff --git a/spec/features/issuables/sorting_list_spec.rb b/spec/features/issuables/sorting_list_spec.rb
index 53723b39d5b..b5362267309 100644
--- a/spec/features/issuables/sorting_list_spec.rb
+++ b/spec/features/issuables/sorting_list_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Sort Issuable List' do
+RSpec.describe 'Sort Issuable List', feature_category: :team_planning do
let(:project) { create(:project, :public) }
let(:first_created_issuable) { issuables.order_created_asc.first }
diff --git a/spec/features/issuables/user_sees_sidebar_spec.rb b/spec/features/issuables/user_sees_sidebar_spec.rb
index 66ed6044de6..2c0157ea9e0 100644
--- a/spec/features/issuables/user_sees_sidebar_spec.rb
+++ b/spec/features/issuables/user_sees_sidebar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue Sidebar on Mobile' do
+RSpec.describe 'Issue Sidebar on Mobile', feature_category: :team_planning do
include MobileHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/issue_rebalancing_spec.rb b/spec/features/issue_rebalancing_spec.rb
index 686aa5eb1b6..3933f0ffcf8 100644
--- a/spec/features/issue_rebalancing_spec.rb
+++ b/spec/features/issue_rebalancing_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue rebalancing' do
+RSpec.describe 'Issue rebalancing', feature_category: :team_planning do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
@@ -41,13 +41,13 @@ RSpec.describe 'Issue rebalancing' do
it 'shows an alert in project issues list with manual sort', :js do
visit project_issues_path(project, sort: 'relative_position')
- expect(page).to have_selector('.flash-notice', text: alert_message_regex, count: 1)
+ expect(page).to have_selector('.gl-alert-info', text: alert_message_regex, count: 1)
end
it 'shows an alert in group issues list with manual sort', :js do
visit issues_group_path(group, sort: 'relative_position')
- expect(page).to have_selector('.flash-notice', text: alert_message_regex, count: 1)
+ expect(page).to have_selector('.gl-alert-info', text: alert_message_regex, count: 1)
end
it 'does not show an alert in project issues list with other sorts' do
diff --git a/spec/features/issues/confidential_notes_spec.rb b/spec/features/issues/confidential_notes_spec.rb
index 858c054c803..d87c73da44d 100644
--- a/spec/features/issues/confidential_notes_spec.rb
+++ b/spec/features/issues/confidential_notes_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "Confidential notes on issues", :js do
+RSpec.describe "Confidential notes on issues", :js, feature_category: :team_planning do
it_behaves_like 'confidential notes on issuables' do
let_it_be(:issuable_parent) { create(:project) }
let_it_be(:issuable) { create(:issue, project: issuable_parent) }
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index a385e8a5fd0..0bdb5930f30 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Resolving all open threads in a merge request from an issue', :js do
+RSpec.describe 'Resolving all open threads in a merge request from an issue', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
index 5ff61a52b21..3a32bd34af8 100644
--- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Resolve an open thread in a merge request by creating an issue', :js do
+RSpec.describe 'Resolve an open thread in a merge request by creating an issue', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/issues/csv_spec.rb b/spec/features/issues/csv_spec.rb
index 9fd171bf44b..8629201459f 100644
--- a/spec/features/issues/csv_spec.rb
+++ b/spec/features/issues/csv_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issues csv', :js do
+RSpec.describe 'Issues csv', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, title: 'v1.0', project: project) }
diff --git a/spec/features/issues/discussion_lock_spec.rb b/spec/features/issues/discussion_lock_spec.rb
index 13f1742fbf6..33fc9a6fd96 100644
--- a/spec/features/issues/discussion_lock_spec.rb
+++ b/spec/features/issues/discussion_lock_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Discussion Lock', :js do
+RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:issue) { create(:issue, project: project, author: user) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 40b0bfd9aa4..a89c36a2b78 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dropdown assignee', :js do
+RSpec.describe 'Dropdown assignee', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index a67d114c6d1..b5d389b3bee 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dropdown author', :js do
+RSpec.describe 'Dropdown author', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/features/issues/filtered_search/dropdown_base_spec.rb b/spec/features/issues/filtered_search/dropdown_base_spec.rb
index 9e3e3d394cd..866b83a6319 100644
--- a/spec/features/issues/filtered_search/dropdown_base_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_base_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dropdown base', :js do
+RSpec.describe 'Dropdown base', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project) { create(:project) }
@@ -10,6 +10,7 @@ RSpec.describe 'Dropdown base', :js do
let_it_be(:issue) { create(:issue, project: project) }
before do
+ stub_feature_flags(or_issuable_queries: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index 78450a9c3f7..52c85942a7c 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dropdown emoji', :js do
+RSpec.describe 'Dropdown emoji', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project) { create(:project, :public) }
@@ -11,6 +11,7 @@ RSpec.describe 'Dropdown emoji', :js do
let_it_be(:award_emoji_star) { create(:award_emoji, name: 'star', user: user, awardable: issue) }
before do
+ stub_feature_flags(or_issuable_queries: false)
project.add_maintainer(user)
create_list(:award_emoji, 2, user: user, name: 'thumbsup')
create_list(:award_emoji, 1, user: user, name: 'thumbsdown')
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index cbe917931aa..39034a40b1f 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dropdown hint', :js do
+RSpec.describe 'Dropdown hint', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index 0ff56909ad1..a2eceb67841 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dropdown label', :js do
+RSpec.describe 'Dropdown label', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project) { create(:project) }
@@ -11,6 +11,7 @@ RSpec.describe 'Dropdown label', :js do
let_it_be(:label) { create(:label, project: project, title: 'bug-label') }
before do
+ stub_feature_flags(or_issuable_queries: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index 37d604106f1..d08eb29b5c0 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dropdown milestone', :js do
+RSpec.describe 'Dropdown milestone', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project) { create(:project) }
@@ -12,6 +12,7 @@ RSpec.describe 'Dropdown milestone', :js do
let_it_be(:issue) { create(:issue, project: project) }
before do
+ stub_feature_flags(or_issuable_queries: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/issues/filtered_search/dropdown_release_spec.rb b/spec/features/issues/filtered_search/dropdown_release_spec.rb
index 08e20563c8e..5d9b8b04012 100644
--- a/spec/features/issues/filtered_search/dropdown_release_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_release_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dropdown release', :js do
+RSpec.describe 'Dropdown release', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project) { create(:project) }
@@ -12,6 +12,7 @@ RSpec.describe 'Dropdown release', :js do
let_it_be(:issue) { create(:issue, project: project) }
before do
+ stub_feature_flags(or_issuable_queries: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index e48df1b1c53..f67d5c40efd 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Filter issues', :js do
+RSpec.describe 'Filter issues', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let(:project) { create(:project) }
diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb
index cb17349dd43..2d9c73f2756 100644
--- a/spec/features/issues/filtered_search/recent_searches_spec.rb
+++ b/spec/features/issues/filtered_search/recent_searches_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Recent searches', :js do
+RSpec.describe 'Recent searches', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project_1) { create(:project, :public) }
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index e075547e326..c975df2a531 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Search bar', :js do
+RSpec.describe 'Search bar', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 854b88c3f81..f25925ed33d 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Visual tokens', :js do
+RSpec.describe 'Visual tokens', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index fe591d7fe3a..2898c97c2e9 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'New/edit issue', :js do
+RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
include ActionView::Helpers::JavaScriptHelper
let_it_be(:project) { create(:project, :repository) }
@@ -43,7 +43,7 @@ RSpec.describe 'New/edit issue', :js do
# To work around this, we have to hold on to and call to the original implementation manually.
original_issue_dropdown_options = FormHelper.instance_method(:assignees_dropdown_options)
allow_any_instance_of(FormHelper).to receive(:assignees_dropdown_options).and_wrap_original do |original, *args|
- options = original_issue_dropdown_options.bind(original.receiver).call(*args)
+ options = original_issue_dropdown_options.bind_call(original.receiver, *args)
options[:data][:per_page] = 2
options
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index fa4ce6fe1c1..2bd5373b715 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'GFM autocomplete', :js do
+RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') }
diff --git a/spec/features/issues/group_label_sidebar_spec.rb b/spec/features/issues/group_label_sidebar_spec.rb
index 8150f9c6faf..b26030fe8d0 100644
--- a/spec/features/issues/group_label_sidebar_spec.rb
+++ b/spec/features/issues/group_label_sidebar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group label on issue' do
+RSpec.describe 'Group label on issue', feature_category: :team_planning do
it 'renders link to the project issues page', :js do
group = create(:group)
project = create(:project, :public, namespace: group)
diff --git a/spec/features/issues/incident_issue_spec.rb b/spec/features/issues/incident_issue_spec.rb
index d6cde466d1b..2fba1ca9141 100644
--- a/spec/features/issues/incident_issue_spec.rb
+++ b/spec/features/issues/incident_issue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Incident Detail', :js do
+RSpec.describe 'Incident Detail', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public) }
let_it_be(:payload) do
{
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index a253e6f4c86..44e9bbad1ba 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue Detail', :js do
+RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
let_it_be_with_refind(:project) { create(:project, :public) }
let(:user) { create(:user) }
diff --git a/spec/features/issues/issue_header_spec.rb b/spec/features/issues/issue_header_spec.rb
index 165015013dd..090067fc4ac 100644
--- a/spec/features/issues/issue_header_spec.rb
+++ b/spec/features/issues/issue_header_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'issue header', :js do
+RSpec.describe 'issue header', :js, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
@@ -27,7 +27,7 @@ RSpec.describe 'issue header', :js do
it 'shows the "New related issue", "Report abuse", and "Delete issue" items', :aggregate_failures do
expect(page).to have_link 'New related issue'
- expect(page).to have_link 'Report abuse'
+ expect(page).to have_link 'Report abuse to administrator'
expect(page).to have_button 'Delete issue'
expect(page).not_to have_link 'Submit as spam'
end
@@ -71,7 +71,7 @@ RSpec.describe 'issue header', :js do
it 'does not show "Report abuse" link in dropdown' do
click_button 'Issue actions'
- expect(page).not_to have_link 'Report abuse'
+ expect(page).not_to have_link 'Report abuse to administrator'
end
end
end
@@ -116,7 +116,7 @@ RSpec.describe 'issue header', :js do
it 'only shows the "New related issue" and "Report abuse" items', :aggregate_failures do
expect(page).to have_link 'New related issue'
- expect(page).to have_link 'Report abuse'
+ expect(page).to have_link 'Report abuse to administrator'
expect(page).not_to have_link 'Submit as spam'
expect(page).not_to have_button 'Delete issue'
end
@@ -160,7 +160,7 @@ RSpec.describe 'issue header', :js do
it 'does not show "Report abuse" link in dropdown' do
click_button 'Issue actions'
- expect(page).not_to have_link 'Report abuse'
+ expect(page).not_to have_link 'Report abuse to administrator'
end
end
end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 6fa8a52a9c5..fa72acad8c6 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue Sidebar' do
+RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
include MobileHelpers
let_it_be(:group) { create(:group, :nested) }
diff --git a/spec/features/issues/issue_state_spec.rb b/spec/features/issues/issue_state_spec.rb
index d5a115433aa..758dafccb86 100644
--- a/spec/features/issues/issue_state_spec.rb
+++ b/spec/features/issues/issue_state_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'issue state', :js do
+RSpec.describe 'issue state', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/issues/keyboard_shortcut_spec.rb b/spec/features/issues/keyboard_shortcut_spec.rb
index 4dbc5d8e01c..f91a0d4b057 100644
--- a/spec/features/issues/keyboard_shortcut_spec.rb
+++ b/spec/features/issues/keyboard_shortcut_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issues shortcut', :js do
+RSpec.describe 'Issues shortcut', :js, feature_category: :team_planning do
context 'New Issue shortcut' do
context 'issues are enabled' do
let(:project) { create(:project) }
diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb
index aad5d319bc4..5cabaf16960 100644
--- a/spec/features/issues/markdown_toolbar_spec.rb
+++ b/spec/features/issues/markdown_toolbar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue markdown toolbar', :js do
+RSpec.describe 'Issue markdown toolbar', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 054b7b3855b..72c6e288168 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'issue move to another project' do
+RSpec.describe 'issue move to another project', feature_category: :team_planning do
let(:user) { create(:user) }
let(:old_project) { create(:project, :repository) }
let(:text) { 'Some issue description' }
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
index 5e02d5ad038..dae71481352 100644
--- a/spec/features/issues/note_polling_spec.rb
+++ b/spec/features/issues/note_polling_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue notes polling', :js do
+RSpec.describe 'Issue notes polling', :js, feature_category: :team_planning do
include NoteInteractionHelpers
let(:project) { create(:project, :public) }
diff --git a/spec/features/issues/notes_on_issues_spec.rb b/spec/features/issues/notes_on_issues_spec.rb
index 4e98062e8b2..8d6262efa53 100644
--- a/spec/features/issues/notes_on_issues_spec.rb
+++ b/spec/features/issues/notes_on_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create notes on issues', :js do
+RSpec.describe 'Create notes on issues', :js, feature_category: :team_planning do
let(:user) { create(:user) }
def submit_comment(text)
diff --git a/spec/features/issues/related_issues_spec.rb b/spec/features/issues/related_issues_spec.rb
index 62127295a7c..f460b4b1c7f 100644
--- a/spec/features/issues/related_issues_spec.rb
+++ b/spec/features/issues/related_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Related issues', :js do
+RSpec.describe 'Related issues', :js, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/issues/resource_label_events_spec.rb b/spec/features/issues/resource_label_events_spec.rb
index e8158b3e2aa..531361b19af 100644
--- a/spec/features/issues/resource_label_events_spec.rb
+++ b/spec/features/issues/resource_label_events_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'List issue resource label events', :js do
+RSpec.describe 'List issue resource label events', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, author: user) }
diff --git a/spec/features/issues/rss_spec.rb b/spec/features/issues/rss_spec.rb
index e3faed81c73..36dffeded50 100644
--- a/spec/features/issues/rss_spec.rb
+++ b/spec/features/issues/rss_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Issues RSS', :js do
+RSpec.describe 'Project Issues RSS', :js, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
diff --git a/spec/features/issues/service_desk_spec.rb b/spec/features/issues/service_desk_spec.rb
index 87cd00fac6b..922ab95538b 100644
--- a/spec/features/issues/service_desk_spec.rb
+++ b/spec/features/issues/service_desk_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Service Desk Issue Tracker', :js do
+RSpec.describe 'Service Desk Issue Tracker', :js, feature_category: :team_planning do
let(:project) { create(:project, :private, service_desk_enabled: true) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/issues/spam_akismet_issue_creation_spec.rb b/spec/features/issues/spam_akismet_issue_creation_spec.rb
index 4cc4c4cf607..7c62f141105 100644
--- a/spec/features/issues/spam_akismet_issue_creation_spec.rb
+++ b/spec/features/issues/spam_akismet_issue_creation_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Spam detection on issue creation', :js do
+RSpec.describe 'Spam detection on issue creation', :js, feature_category: :team_planning do
include StubENV
let(:project) { create(:project, :public) }
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 6a53c12eda3..2c537cefa5e 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Manually create a todo item from issue', :js do
+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) }
diff --git a/spec/features/issues/user_bulk_edits_issues_labels_spec.rb b/spec/features/issues/user_bulk_edits_issues_labels_spec.rb
index 2a201e0bc23..1fc6609d1f5 100644
--- a/spec/features/issues/user_bulk_edits_issues_labels_spec.rb
+++ b/spec/features/issues/user_bulk_edits_issues_labels_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issues > Labels bulk assignment' do
+RSpec.describe 'Issues > Labels bulk assignment', feature_category: :team_planning do
let(:user) { create(:user) }
let!(:project) { create(:project) }
let!(:bug) { create(:label, project: project, title: 'bug') }
diff --git a/spec/features/issues/user_bulk_edits_issues_spec.rb b/spec/features/issues/user_bulk_edits_issues_spec.rb
index d7fad355cb4..fc48bc4baf9 100644
--- a/spec/features/issues/user_bulk_edits_issues_spec.rb
+++ b/spec/features/issues/user_bulk_edits_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Multiple issue updating from issues#index', :js do
+RSpec.describe 'Multiple issue updating from issues#index', :js, feature_category: :team_planning do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user) }
diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb
index ef00e66af7e..59e1413fc97 100644
--- a/spec/features/issues/user_comments_on_issue_spec.rb
+++ b/spec/features/issues/user_comments_on_issue_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User comments on issue", :js do
+RSpec.describe "User comments on issue", :js, feature_category: :team_planning do
include Spec::Support::Helpers::Features::NotesHelpers
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
index 5ba09703852..bbc14368d82 100644
--- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User creates branch and merge request on issue page', :js do
+RSpec.describe 'User creates branch and merge request on issue page', :js, feature_category: :team_planning do
let(:membership_level) { :developer }
let(:user) { create(:user) }
let!(:project) { create(:project, :repository, :public) }
@@ -84,7 +84,7 @@ RSpec.describe 'User creates branch and merge request on issue page', :js do
wait_for_requests
- expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk')
+ expect(page).to have_selector('.ref-selector ', text: '1-cherry-coloured-funk')
expect(page).to have_current_path project_tree_path(project, '1-cherry-coloured-funk'), ignore_query: true
end
end
@@ -109,7 +109,7 @@ RSpec.describe 'User creates branch and merge request on issue page', :js do
wait_for_requests
- expect(page).to have_selector('.dropdown-toggle-text ', text: branch_name)
+ expect(page).to have_selector('.ref-selector', text: branch_name)
expect(page).to have_current_path project_tree_path(project, branch_name), ignore_query: true
end
end
diff --git a/spec/features/issues/user_creates_confidential_merge_request_spec.rb b/spec/features/issues/user_creates_confidential_merge_request_spec.rb
index 6b4526cd624..23fef5fa46e 100644
--- a/spec/features/issues/user_creates_confidential_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_confidential_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User creates confidential merge request on issue page', :js do
+RSpec.describe 'User creates confidential merge request on issue page', :js, feature_category: :team_planning do
include ProjectForksHelper
let(:user) { create(:user) }
diff --git a/spec/features/issues/user_creates_issue_by_email_spec.rb b/spec/features/issues/user_creates_issue_by_email_spec.rb
index c47f24ab836..d6d2b2a50f8 100644
--- a/spec/features/issues/user_creates_issue_by_email_spec.rb
+++ b/spec/features/issues/user_creates_issue_by_email_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issues > User creates issue by email' do
+RSpec.describe 'Issues > User creates issue by email', feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index 1d023a15159..a4b8cb91999 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User creates issue" do
+RSpec.describe "User creates issue", feature_category: :team_planning do
include DropzoneHelper
let_it_be(:project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index 75df85f362f..223832a6ede 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "Issues > User edits issue", :js do
+RSpec.describe "Issues > User edits issue", :js, feature_category: :team_planning do
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:project_with_milestones) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
@@ -416,7 +416,7 @@ RSpec.describe "Issues > User edits issue", :js do
find('.gl-form-input', visible: true).send_keys "\"#{milestones[0].title}\""
wait_for_requests
- page.within '.gl-new-dropdown-contents' do
+ page.within '.gl-dropdown-contents' do
expect(page).to have_content milestones[0].title
end
end
diff --git a/spec/features/issues/user_filters_issues_spec.rb b/spec/features/issues/user_filters_issues_spec.rb
index 2941ea6ec36..9f69e94b86c 100644
--- a/spec/features/issues/user_filters_issues_spec.rb
+++ b/spec/features/issues/user_filters_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User filters issues', :js do
+RSpec.describe 'User filters issues', :js, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb
index a2dea7f048b..539e429534e 100644
--- a/spec/features/issues/user_interacts_with_awards_spec.rb
+++ b/spec/features/issues/user_interacts_with_awards_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User interacts with awards' do
+RSpec.describe 'User interacts with awards', feature_category: :team_planning do
include MobileHelpers
let(:user) { create(:user) }
@@ -136,6 +136,10 @@ RSpec.describe 'User interacts with awards' do
page.within('.note-actions') do
find('.note-emoji-button').click
end
+
+ # make sure emoji popup is visible
+ execute_script("window.scrollBy(0, 200)")
+
find('gl-emoji[data-name="8ball"]').click
wait_for_requests
diff --git a/spec/features/issues/user_resets_their_incoming_email_token_spec.rb b/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
index 4580378dc8a..55c66eb8a39 100644
--- a/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
+++ b/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issues > User resets their incoming email token' do
+RSpec.describe 'Issues > User resets their incoming email token', feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, namespace: user.namespace) }
let_it_be(:issue) { create(:issue, project: project) }
diff --git a/spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb b/spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb
index 1fa8f533869..f93fbd06964 100644
--- a/spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb
+++ b/spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User scrolls to deep-linked note' do
+RSpec.describe 'User scrolls to deep-linked note', feature_category: :team_planning do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:comment_1) { create(:note_on_issue, noteable: issue, project: project, note: 'written first') }
diff --git a/spec/features/issues/user_sees_breadcrumb_links_spec.rb b/spec/features/issues/user_sees_breadcrumb_links_spec.rb
index 4ec13533a8d..632999c5d49 100644
--- a/spec/features/issues/user_sees_breadcrumb_links_spec.rb
+++ b/spec/features/issues/user_sees_breadcrumb_links_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'New issue breadcrumb' do
+RSpec.describe 'New issue breadcrumb', feature_category: :team_planning do
let_it_be(:project, reload: true) { create(:project) }
let(:user) { project.creator }
diff --git a/spec/features/issues/user_sees_empty_state_spec.rb b/spec/features/issues/user_sees_empty_state_spec.rb
index b4c5a57de4f..5b95eb96e3b 100644
--- a/spec/features/issues/user_sees_empty_state_spec.rb
+++ b/spec/features/issues/user_sees_empty_state_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issues > User sees empty state', :js do
+RSpec.describe 'Issues > User sees empty state', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { project.creator }
diff --git a/spec/features/issues/user_sees_live_update_spec.rb b/spec/features/issues/user_sees_live_update_spec.rb
index 7e4880f209e..860603ad546 100644
--- a/spec/features/issues/user_sees_live_update_spec.rb
+++ b/spec/features/issues/user_sees_live_update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issues > User sees live update', :js do
+RSpec.describe 'Issues > User sees live update', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { project.creator }
diff --git a/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
index 311818d2d15..b9a25f47da9 100644
--- a/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
+++ b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issues > Real-time sidebar', :js do
+RSpec.describe 'Issues > Real-time sidebar', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/issues/user_sorts_issue_comments_spec.rb b/spec/features/issues/user_sorts_issue_comments_spec.rb
index 4b38ce329b8..ca52e620ea7 100644
--- a/spec/features/issues/user_sorts_issue_comments_spec.rb
+++ b/spec/features/issues/user_sorts_issue_comments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Comment sort direction' do
+RSpec.describe 'Comment sort direction', feature_category: :team_planning do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:comment_1) { create(:note_on_issue, noteable: issue, project: project, note: 'written first') }
diff --git a/spec/features/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb
index 2716d742be3..206544b32a4 100644
--- a/spec/features/issues/user_sorts_issues_spec.rb
+++ b/spec/features/issues/user_sorts_issues_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User sorts issues" do
+RSpec.describe "User sorts issues", feature_category: :team_planning do
include SortingHelper
include IssueHelpers
diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb
index 541bbc8a8e7..904fafdf56a 100644
--- a/spec/features/issues/user_toggles_subscription_spec.rb
+++ b/spec/features/issues/user_toggles_subscription_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User toggles subscription", :js do
+RSpec.describe "User toggles subscription", :js, feature_category: :team_planning do
let(:project) { create(:project_empty_repo, :public) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb
index d458c991668..963f1c56fef 100644
--- a/spec/features/issues/user_uses_quick_actions_spec.rb
+++ b/spec/features/issues/user_uses_quick_actions_spec.rb
@@ -7,7 +7,7 @@ require 'spec_helper'
# for example, adding quick actions when creating the issue and checking DateTime formats on UI.
# Because this kind of spec takes more time to run there is no need to add new ones
# for each existing quick action unless they test something not tested by existing tests.
-RSpec.describe 'Issues > User uses quick actions', :js do
+RSpec.describe 'Issues > User uses quick actions', :js, feature_category: :team_planning do
include Spec::Support::Helpers::Features::NotesHelpers
context "issuable common quick actions" do
diff --git a/spec/features/issues/user_views_issue_spec.rb b/spec/features/issues/user_views_issue_spec.rb
index eca698bb2f4..17ff3e0c702 100644
--- a/spec/features/issues/user_views_issue_spec.rb
+++ b/spec/features/issues/user_views_issue_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User views issue" do
+RSpec.describe "User views issue", feature_category: :team_planning do
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, project: project, description: "# Description header\n\n**Lorem** _ipsum_ dolor sit [amet](https://example.com)", author: user) }
diff --git a/spec/features/issues/user_views_issues_spec.rb b/spec/features/issues/user_views_issues_spec.rb
index 56afa7eb6ba..39d3dfbd487 100644
--- a/spec/features/issues/user_views_issues_spec.rb
+++ b/spec/features/issues/user_views_issues_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User views issues" do
+RSpec.describe "User views issues", feature_category: :team_planning do
let!(:closed_issue) { create(:closed_issue, project: project) }
let!(:open_issue1) { create(:issue, project: project) }
let!(:open_issue2) { create(:issue, project: project) }
diff --git a/spec/features/jira_connect/branches_spec.rb b/spec/features/jira_connect/branches_spec.rb
index c334a425849..489d3743a2a 100644
--- a/spec/features/jira_connect/branches_spec.rb
+++ b/spec/features/jira_connect/branches_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create GitLab branches from Jira', :js do
+RSpec.describe 'Create GitLab branches from Jira', :js, feature_category: :integrations do
let_it_be(:alice) { create(:user, name: 'Alice') }
let_it_be(:bob) { create(:user, name: 'Bob') }
@@ -49,16 +49,11 @@ RSpec.describe 'Create GitLab branches from Jira', :js do
expect(page).to have_button('Create branch', disabled: false)
click_on 'master'
+ fill_in 'Search', with: source_branch
+ expect(page).not_to have_text(source_branch)
- within_dropdown do
- fill_in 'Search', with: source_branch
-
- expect(page).not_to have_text(source_branch)
-
- fill_in 'Search', with: 'master'
-
- expect(page).to have_text('master')
- end
+ fill_in 'Search', with: 'master'
+ expect(page).to have_text('master')
# Switch to project2
@@ -70,10 +65,13 @@ RSpec.describe 'Create GitLab branches from Jira', :js do
end
click_on 'master'
+ wait_for_requests
- within_dropdown do
- fill_in 'Search', with: source_branch
- click_on source_branch
+ fill_in 'Search', with: source_branch
+ wait_for_requests
+
+ within '[role="listbox"]' do
+ find('li', text: source_branch).click
end
fill_in 'Branch name', with: new_branch
diff --git a/spec/features/jira_connect/subscriptions_spec.rb b/spec/features/jira_connect/subscriptions_spec.rb
index 0468cfd70fc..8686234df01 100644
--- a/spec/features/jira_connect/subscriptions_spec.rb
+++ b/spec/features/jira_connect/subscriptions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Subscriptions Content Security Policy' do
+RSpec.describe 'Subscriptions Content Security Policy', feature_category: :integrations do
include ContentSecurityPolicyHelpers
let(:installation) { create(:jira_connect_installation) }
diff --git a/spec/features/jira_oauth_provider_authorize_spec.rb b/spec/features/jira_oauth_provider_authorize_spec.rb
index eb26440aff9..a542aaa7619 100644
--- a/spec/features/jira_oauth_provider_authorize_spec.rb
+++ b/spec/features/jira_oauth_provider_authorize_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'JIRA OAuth Provider' do
+RSpec.describe 'JIRA OAuth Provider', feature_category: :integrations do
describe 'JIRA DVCS OAuth Authorization' do
let(:application) { create(:oauth_application, redirect_uri: oauth_jira_dvcs_callback_url, scopes: 'read_user') }
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index 2f22ac8b395..d6e607e80df 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Labels Hierarchy', :js do
+RSpec.describe 'Labels Hierarchy', :js, feature_category: :team_planning do
include FilteredSearchHelpers
let!(:user) { create(:user) }
@@ -17,6 +17,7 @@ RSpec.describe 'Labels Hierarchy', :js do
let!(:project_label_1) { create(:label, project: project_1, title: 'Label_4') }
before do
+ stub_feature_flags(or_issuable_queries: false)
grandparent.add_owner(user)
sign_in(user)
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index b5bf9279371..8073e7e9556 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Copy as GFM', :js do
+RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do
include MarkupHelper
include RepoHelpers
include ActionView::Helpers::JavaScriptHelper
@@ -26,7 +26,7 @@ RSpec.describe 'Copy as GFM', :js do
# by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
# These are all in a single `it` for performance reasons.
- it 'works', :aggregate_failures do
+ it 'transforms HTML to GFM', :aggregate_failures do
verify(
'nesting',
'> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
@@ -459,25 +459,25 @@ RSpec.describe 'Copy as GFM', :js do
</a>
</div>
<!---->
- <button type="button" class="btn qa-apply-btn js-apply-btn">Apply suggestion</button>
+ <button type="button" class="btn js-apply-btn">Apply suggestion</button>
</div>
<table class="mb-3 md-suggestion-diff js-syntax-highlight code white">
<tbody>
<tr class="line_holder old">
- <td class="diff-line-num old_line qa-old-diff-line-number old">9</td>
+ <td class="diff-line-num old_line old">9</td>
<td class="diff-line-num new_line old"></td>
<td class="line_content old"><span>Old
</span></td>
</tr>
<tr class="line_holder new">
<td class="diff-line-num old_line new"></td>
- <td class="diff-line-num new_line qa-new-diff-line-number new">9</td>
+ <td class="diff-line-num new_line new">9</td>
<td class="line_content new"><span>New
</span></td>
</tr>
<tr class="line_holder new">
<td class="diff-line-num old_line new"></td>
- <td class="diff-line-num new_line qa-new-diff-line-number new">10</td>
+ <td class="diff-line-num new_line new">10</td>
<td class="line_content new"><span> And newer
</span></td>
</tr>
diff --git a/spec/features/markdown/gitlab_flavored_markdown_spec.rb b/spec/features/markdown/gitlab_flavored_markdown_spec.rb
index 17fe2dab8f7..36b02b17924 100644
--- a/spec/features/markdown/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/markdown/gitlab_flavored_markdown_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "GitLab Flavored Markdown" do
+RSpec.describe "GitLab Flavored Markdown", feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/features/markdown/json_table_spec.rb b/spec/features/markdown/json_table_spec.rb
index 6b74dbac255..a9afbe1fc57 100644
--- a/spec/features/markdown/json_table_spec.rb
+++ b/spec/features/markdown/json_table_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Rendering json:table code block in markdown', :js do
+RSpec.describe 'Rendering json:table code block in markdown', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public) }
it 'creates table correctly' do
diff --git a/spec/features/markdown/keyboard_shortcuts_spec.rb b/spec/features/markdown/keyboard_shortcuts_spec.rb
index 82288af1f9f..cfb8e61689f 100644
--- a/spec/features/markdown/keyboard_shortcuts_spec.rb
+++ b/spec/features/markdown/keyboard_shortcuts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Markdown keyboard shortcuts', :js do
+RSpec.describe 'Markdown keyboard shortcuts', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/markdown/kroki_spec.rb b/spec/features/markdown/kroki_spec.rb
index f02f5d44244..dca00c5f2fa 100644
--- a/spec/features/markdown/kroki_spec.rb
+++ b/spec/features/markdown/kroki_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'kroki rendering', :js do
+RSpec.describe 'kroki rendering', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public) }
before do
diff --git a/spec/features/markdown/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb
index 08f9b8eda13..132a03877f8 100644
--- a/spec/features/markdown/markdown_spec.rb
+++ b/spec/features/markdown/markdown_spec.rb
@@ -26,7 +26,7 @@ require 'erb'
#
# See the MarkdownFeature class for setup details.
-RSpec.describe 'GitLab Markdown', :aggregate_failures do
+RSpec.describe 'GitLab Markdown', :aggregate_failures, feature_category: :team_planning do
include Capybara::Node::Matchers
include MarkupHelper
include MarkdownMatchers
@@ -290,6 +290,13 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures do
aggregate_failures 'KrokiFilter' do
expect(doc).to parse_kroki
end
+
+ aggregate_failures 'AttributeFilter' do
+ img = doc.at_css('img[alt="Sized Image"]')
+
+ expect(img.attr('width')).to eq('75%')
+ expect(img.attr('height')).to eq('100')
+ end
end
end
diff --git a/spec/features/markdown/math_spec.rb b/spec/features/markdown/math_spec.rb
index 1f219886818..0c77bd2a8ff 100644
--- a/spec/features/markdown/math_spec.rb
+++ b/spec/features/markdown/math_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Math rendering', :js do
+RSpec.describe 'Math rendering', :js, feature_category: :team_planning do
let!(:project) { create(:project, :public) }
it 'renders inline and display math correctly' do
diff --git a/spec/features/markdown/metrics_spec.rb b/spec/features/markdown/metrics_spec.rb
index 61dd41204f8..b5e42b16f87 100644
--- a/spec/features/markdown/metrics_spec.rb
+++ b/spec/features/markdown/metrics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_store_caching, :sidekiq_inline do
+RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_store_caching, :sidekiq_inline, feature_category: :team_planning do
include PrometheusHelpers
include KubernetesHelpers
include GrafanaApiHelpers
diff --git a/spec/features/markdown/observability_spec.rb b/spec/features/markdown/observability_spec.rb
new file mode 100644
index 00000000000..0c7d8cc006b
--- /dev/null
+++ b/spec/features/markdown/observability_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Observability rendering', :js do
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:observable_url) { "https://observe.gitlab.com/" }
+
+ let_it_be(:expected) do
+ %(<iframe src="#{observable_url}?theme=light&amp;kiosk" frameborder="0")
+ end
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context 'when embedding in an issue' do
+ let(:issue) do
+ create(:issue, project: project, description: observable_url)
+ end
+
+ before do
+ visit project_issue_path(project, issue)
+ wait_for_requests
+ end
+
+ it 'renders iframe in description' do
+ page.within('.description') do
+ expect(page.html).to include(expected)
+ end
+ end
+
+ it 'renders iframe in comment' do
+ expect(page).not_to have_css('.note-text')
+
+ page.within('.js-main-target-form') do
+ fill_in('note[note]', with: observable_url)
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.note-text') do
+ expect(page.html).to include(expected)
+ end
+ end
+ end
+
+ context 'when embedding in an MR' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project, target_project: project, description: observable_url)
+ end
+
+ before do
+ visit merge_request_path(merge_request)
+ wait_for_requests
+ end
+
+ it 'renders iframe in description' do
+ page.within('.description') do
+ expect(page.html).to include(expected)
+ end
+ end
+
+ it 'renders iframe in comment' do
+ expect(page).not_to have_css('.note-text')
+
+ page.within('.js-main-target-form') do
+ fill_in('note[note]', with: observable_url)
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.note-text') do
+ expect(page.html).to include(expected)
+ end
+ end
+ end
+end
diff --git a/spec/features/markdown/sandboxed_mermaid_spec.rb b/spec/features/markdown/sandboxed_mermaid_spec.rb
index 2bf88d7882d..26b397a1fd5 100644
--- a/spec/features/markdown/sandboxed_mermaid_spec.rb
+++ b/spec/features/markdown/sandboxed_mermaid_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Sandboxed Mermaid rendering', :js do
+RSpec.describe 'Sandboxed Mermaid rendering', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:description) do
<<~MERMAID
diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb
index f01217df8c5..e16c1ae094b 100644
--- a/spec/features/merge_request/batch_comments_spec.rb
+++ b/spec/features/merge_request/batch_comments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > Batch comments', :js do
+RSpec.describe 'Merge request > Batch comments', :js, feature_category: :code_review do
include MergeRequestDiffHelpers
include RepoHelpers
diff --git a/spec/features/merge_request/close_reopen_report_toggle_spec.rb b/spec/features/merge_request/close_reopen_report_toggle_spec.rb
index 5e9400935c3..63ed355b16e 100644
--- a/spec/features/merge_request/close_reopen_report_toggle_spec.rb
+++ b/spec/features/merge_request/close_reopen_report_toggle_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issuables Close/Reopen/Report toggle' do
+RSpec.describe 'Issuables Close/Reopen/Report toggle', feature_category: :code_review do
include IssuablesHelper
let(:user) { create(:user) }
@@ -27,14 +27,14 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
find('[data-testid="merge-request-actions"]').click
expect(container).to have_link("Close merge request")
- expect(container).to have_link('Report abuse')
+ expect(container).to have_link('Report abuse to administrator')
end
it 'links to Report Abuse' do
find('[data-testid="merge-request-actions"]').click
- click_link 'Report abuse'
+ click_link 'Report abuse to administrator'
- expect(page).to have_content('Report abuse to admin')
+ expect(page).to have_content('Report abuse to administrator')
end
end
@@ -47,7 +47,7 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
expect(container).to have_link('Edit')
expect(container).to have_link('Mark as draft')
expect(container).to have_link('Close merge request')
- expect(container).to have_link('Report abuse')
+ expect(container).to have_link('Report abuse to administrator')
expect(container).not_to have_link('Reopen merge request')
end
end
@@ -59,7 +59,7 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
find('[data-testid="merge-request-actions"]').click
expect(container).to have_link('Edit')
- expect(container).to have_link('Report abuse')
+ expect(container).to have_link('Report abuse to administrator')
expect(container).to have_link('Reopen merge request')
expect(container).not_to have_link('Close merge request')
end
@@ -73,7 +73,7 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
expect(container).to have_link('Edit')
expect(container).to have_link('Reopen merge request')
expect(container).not_to have_link('Close merge request')
- expect(container).not_to have_link('Report abuse')
+ expect(container).not_to have_link('Report abuse to administrator')
end
end
end
@@ -83,7 +83,7 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
it 'shows only the `Edit` button' do
expect(container).to have_link(exact_text: 'Edit')
- expect(container).not_to have_link('Report abuse')
+ expect(container).not_to have_link('Report abuse to administrator')
expect(container).not_to have_button('Close merge request')
expect(container).not_to have_button('Reopen merge request')
end
@@ -93,7 +93,7 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
it 'shows only the `Edit` button' do
expect(container).to have_link(exact_text: 'Edit')
- expect(container).not_to have_link('Report abuse')
+ expect(container).not_to have_link('Report abuse to administrator')
expect(container).not_to have_button('Close merge request')
expect(container).not_to have_button('Reopen merge request')
end
@@ -112,7 +112,7 @@ RSpec.describe 'Issuables Close/Reopen/Report toggle' do
end
it 'only shows a `Report abuse` button' do
- expect(container).to have_link('Report abuse')
+ expect(container).to have_link('Report abuse to administrator')
expect(container).not_to have_button('Close merge request')
expect(container).not_to have_button('Reopen merge request')
expect(container).not_to have_link(exact_text: 'Edit')
diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb
index 39d948bb6fb..bd040a5b894 100644
--- a/spec/features/merge_request/maintainer_edits_fork_spec.rb
+++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork', :js, :sidekiq_might_not_need_inline do
+RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork', :js, :sidekiq_might_not_need_inline,
+feature_category: :code_review do
include Spec::Support::Helpers::Features::SourceEditorSpecHelpers
include ProjectForksHelper
let(:user) { create(:user, username: 'the-maintainer') }
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 d69295744f7..b48d4d80647 100644
--- a/spec/features/merge_request/merge_request_discussion_lock_spec.rb
+++ b/spec/features/merge_request/merge_request_discussion_lock_spec.rb
@@ -4,7 +4,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Request Discussion Lock', :js do
+RSpec.describe 'Merge Request Discussion Lock', :js, feature_category: :code_review do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, author: user) }
diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb
index b50e6779e07..dda22abada0 100644
--- a/spec/features/merge_request/user_accepts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inline do
+RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inline, feature_category: :code_review do
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
index 75912238501..cf6836b544b 100644
--- a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
+++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'create a merge request, allowing commits from members who can merge to the target branch', :js do
+RSpec.describe 'create a merge request, allowing commits from members who can merge to the target branch', :js,
+feature_category: :code_review do
include ProjectForksHelper
let(:user) { create(:user) }
let(:target_project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_request/user_approves_spec.rb b/spec/features/merge_request/user_approves_spec.rb
index 9670012803e..bfb6a3ec8de 100644
--- a/spec/features/merge_request/user_approves_spec.rb
+++ b/spec/features/merge_request/user_approves_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User approves', :js do
+RSpec.describe 'Merge request > User approves', :js, feature_category: :code_review do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_request/user_assigns_themselves_reviewer_spec.rb b/spec/features/merge_request/user_assigns_themselves_reviewer_spec.rb
new file mode 100644
index 00000000000..2b93f88e96b
--- /dev/null
+++ b/spec/features/merge_request/user_assigns_themselves_reviewer_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Merge request > User assigns themselves as a reviewer', feature_category: :code_review do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let(:user) { project.creator }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "test mr") }
+
+ context 'when logged in as a member of the project' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'updates updated_by', :js do
+ wait_for_requests
+
+ expect do
+ page.within('.reviewer') do
+ click_button 'assign yourself'
+ end
+
+ expect(find('.reviewer')).to have_content(user.name)
+ wait_for_all_requests
+ end.to change { merge_request.reload.updated_at }
+ end
+
+ context 'when logged in as a non-member of the project' do
+ before do
+ sign_in(create(:user))
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'does not show link to assign self as Reviewer' do
+ page.within('.reviewer') do
+ expect(page).not_to have_content 'Assign yourself'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_request/user_assigns_themselves_spec.rb b/spec/features/merge_request/user_assigns_themselves_spec.rb
index 2aaddc7791b..826904bd165 100644
--- a/spec/features/merge_request/user_assigns_themselves_spec.rb
+++ b/spec/features/merge_request/user_assigns_themselves_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User assigns themselves' do
+RSpec.describe 'Merge request > User assigns themselves', feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:issue1) { create(:issue, project: project) }
@@ -22,8 +22,12 @@ RSpec.describe 'Merge request > User assigns themselves' do
end
it 'updates updated_by', :js do
+ wait_for_requests
+
expect do
- click_button 'assign yourself'
+ page.within('[data-testid="assignee-block-container"]') do
+ click_button 'assign yourself'
+ end
expect(find('.assignee')).to have_content(user.name)
wait_for_all_requests
@@ -36,7 +40,9 @@ RSpec.describe 'Merge request > User assigns themselves' do
end
it 'does not display if related issues are already assigned' do
- expect(page).not_to have_content 'Assign yourself'
+ page.within('[data-testid="assignee-block-container"]') do
+ expect(page).not_to have_content 'Assign yourself'
+ end
end
end
end
@@ -48,7 +54,9 @@ RSpec.describe 'Merge request > User assigns themselves' do
end
it 'does not show assignment link' do
- expect(page).not_to have_content 'Assign yourself'
+ page.within('[data-testid="assignee-block-container"]') do
+ expect(page).not_to have_content 'Assign yourself'
+ end
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 6fdc1a29174..dceac8d6a69 100644
--- a/spec/features/merge_request/user_awards_emoji_spec.rb
+++ b/spec/features/merge_request/user_awards_emoji_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User awards emoji', :js do
+RSpec.describe 'Merge request > User awards emoji', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project, author: create(:user)) }
diff --git a/spec/features/merge_request/user_clicks_merge_request_tabs_spec.rb b/spec/features/merge_request/user_clicks_merge_request_tabs_spec.rb
index f3cbc1ea1f5..3e3ff91ad19 100644
--- a/spec/features/merge_request/user_clicks_merge_request_tabs_spec.rb
+++ b/spec/features/merge_request/user_clicks_merge_request_tabs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User clicks on merge request tabs', :js do
+RSpec.describe 'User clicks on merge request tabs', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
diff --git a/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb b/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb
index 70951982c22..c5ef6b912fe 100644
--- a/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb
+++ b/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'User closes/reopens a merge request', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297500' do
+RSpec.describe 'User closes/reopens a merge request', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297500',
+ feature_category: :code_review do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/merge_request/user_comments_on_commit_spec.rb b/spec/features/merge_request/user_comments_on_commit_spec.rb
index 8fa1fe3812d..64fe144cd0d 100644
--- a/spec/features/merge_request/user_comments_on_commit_spec.rb
+++ b/spec/features/merge_request/user_comments_on_commit_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User comments on a commit', :js do
+RSpec.describe 'User comments on a commit', :js, feature_category: :code_review do
include MergeRequestDiffHelpers
include RepoHelpers
diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb
index ffaf403e873..f1a942d5708 100644
--- a/spec/features/merge_request/user_comments_on_diff_spec.rb
+++ b/spec/features/merge_request/user_comments_on_diff_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User comments on a diff', :js do
+RSpec.describe 'User comments on a diff', :js, feature_category: :code_review do
include MergeRequestDiffHelpers
include RepoHelpers
diff --git a/spec/features/merge_request/user_comments_on_merge_request_spec.rb b/spec/features/merge_request/user_comments_on_merge_request_spec.rb
index dbcfc2b968f..d5ad78746f4 100644
--- a/spec/features/merge_request/user_comments_on_merge_request_spec.rb
+++ b/spec/features/merge_request/user_comments_on_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User comments on a merge request', :js do
+RSpec.describe 'User comments on a merge request', :js, feature_category: :code_review do
include RepoHelpers
let(:project) { create(:project, :repository) }
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index bd5048374d5..eb7894f4ef7 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User creates image diff notes', :js do
+RSpec.describe 'Merge request > User creates image diff notes', :js, feature_category: :code_review do
include NoteInteractionHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index 0ae4ef18649..50629f11959 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User creates a merge request', :js do
+RSpec.describe 'User creates a merge request', :js, feature_category: :code_review do
include ProjectForksHelper
shared_examples 'creates a merge request' do
@@ -77,7 +77,7 @@ RSpec.describe 'User creates a merge request', :js do
first('.dropdown-source-project a', text: forked_project.full_path)
first('.js-target-project').click
- first('.dropdown-target-project a', text: project.full_path)
+ first('.dropdown-target-project li', text: project.full_path)
first('.js-source-branch').click
diff --git a/spec/features/merge_request/user_creates_mr_spec.rb b/spec/features/merge_request/user_creates_mr_spec.rb
index 9d97e57fe3a..5effde234cd 100644
--- a/spec/features/merge_request/user_creates_mr_spec.rb
+++ b/spec/features/merge_request/user_creates_mr_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User creates MR' do
+RSpec.describe 'Merge request > User creates MR', feature_category: :code_review do
include ProjectForksHelper
before do
diff --git a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
index f0c0142a6cc..4f1119d6c33 100644
--- a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
+++ b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request < User customizes merge commit message', :js do
+RSpec.describe 'Merge request < User customizes merge commit message', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:issue_1) { create(:issue, project: project) }
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 59b5923b2a1..c04040dd6fd 100644
--- a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
+++ b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User edits assignees sidebar', :js do
+RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:protected_branch) { create(:protected_branch, :maintainers_can_push, name: 'master', project: project) }
let(:merge_request) { create(:merge_request, :simple, source_project: project, target_branch: protected_branch.name) }
diff --git a/spec/features/merge_request/user_edits_merge_request_spec.rb b/spec/features/merge_request/user_edits_merge_request_spec.rb
index 4ac25ea7ae0..6701c7d91ae 100644
--- a/spec/features/merge_request/user_edits_merge_request_spec.rb
+++ b/spec/features/merge_request/user_edits_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User edits a merge request', :js do
+RSpec.describe 'User edits a merge request', :js, feature_category: :code_review do
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
diff --git a/spec/features/merge_request/user_edits_mr_spec.rb b/spec/features/merge_request/user_edits_mr_spec.rb
index 2c949ed84f4..18e6827a872 100644
--- a/spec/features/merge_request/user_edits_mr_spec.rb
+++ b/spec/features/merge_request/user_edits_mr_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User edits MR' do
+RSpec.describe 'Merge request > User edits MR', feature_category: :code_review do
include ProjectForksHelper
before do
diff --git a/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb b/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb
index caf0c609f64..38c76314b9e 100644
--- a/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb
+++ b/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User edits reviewers sidebar', :js do
+RSpec.describe 'Merge request > User edits reviewers sidebar', :js, feature_category: :code_review do
context 'with invite members considerations' do
let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:project) { merge_request.project }
diff --git a/spec/features/merge_request/user_expands_diff_spec.rb b/spec/features/merge_request/user_expands_diff_spec.rb
index 25c9584350d..8adbdcd310c 100644
--- a/spec/features/merge_request/user_expands_diff_spec.rb
+++ b/spec/features/merge_request/user_expands_diff_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User expands diff', :js do
+RSpec.describe 'User expands diff', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_branch: 'expand-collapse-files', source_project: project, target_project: project) }
diff --git a/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb b/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb
index 07d99a786ba..1b9b3941714 100644
--- a/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb
+++ b/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Batch diffs', :js do
+RSpec.describe 'Batch diffs', :js, feature_category: :code_review do
include MergeRequestDiffHelpers
include RepoHelpers
diff --git a/spec/features/merge_request/user_locks_discussion_spec.rb b/spec/features/merge_request/user_locks_discussion_spec.rb
index c8a6fdd4007..1bfd52d49e8 100644
--- a/spec/features/merge_request/user_locks_discussion_spec.rb
+++ b/spec/features/merge_request/user_locks_discussion_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User locks discussion', :js do
+RSpec.describe 'Merge request > User locks discussion', :js, feature_category: :code_review do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_request/user_manages_subscription_spec.rb b/spec/features/merge_request/user_manages_subscription_spec.rb
index a8d59a6ffb5..16d869fc5a1 100644
--- a/spec/features/merge_request/user_manages_subscription_spec.rb
+++ b/spec/features/merge_request/user_manages_subscription_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User manages subscription', :js do
+RSpec.describe 'User manages subscription', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
diff --git a/spec/features/merge_request/user_marks_merge_request_as_draft_spec.rb b/spec/features/merge_request/user_marks_merge_request_as_draft_spec.rb
index d85f275b724..201cdc94b56 100644
--- a/spec/features/merge_request/user_marks_merge_request_as_draft_spec.rb
+++ b/spec/features/merge_request/user_marks_merge_request_as_draft_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User marks merge request as draft', :js do
+RSpec.describe 'Merge request > User marks merge request as draft', :js, feature_category: :code_review do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb
index 91327059e0f..b0aeea997f0 100644
--- a/spec/features/merge_request/user_merges_immediately_spec.rb
+++ b/spec/features/merge_request/user_merges_immediately_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge requests > User merges immediately', :js do
+RSpec.describe 'Merge requests > User merges immediately', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let!(:merge_request) do
diff --git a/spec/features/merge_request/user_merges_merge_request_spec.rb b/spec/features/merge_request/user_merges_merge_request_spec.rb
index c91dc7b1c00..4196fdd5dac 100644
--- a/spec/features/merge_request/user_merges_merge_request_spec.rb
+++ b/spec/features/merge_request/user_merges_merge_request_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User merges a merge request", :js do
+RSpec.describe "User merges a merge request", :js, feature_category: :code_review do
let(:user) { project.first_owner }
before do
diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
index d6b132b18da..447418b5a4b 100644
--- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do
+RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, feature_category: :code_review do
let(:merge_request) { create(:merge_request_with_diffs) }
let(:project) { merge_request.target_project }
diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
index abf916c72b3..78a21527794 100644
--- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
+RSpec.describe 'Merge request > User merges when pipeline succeeds', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) do
diff --git a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
index 4d2c59665bb..116de50f2a2 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User opens checkout branch modal', :js do
+RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_category: :code_review do
include ProjectForksHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_request/user_opens_context_commits_modal_spec.rb b/spec/features/merge_request/user_opens_context_commits_modal_spec.rb
index 2d574e57fe9..f32a51cfcd4 100644
--- a/spec/features/merge_request/user_opens_context_commits_modal_spec.rb
+++ b/spec/features/merge_request/user_opens_context_commits_modal_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > Context commits', :js do
+RSpec.describe 'Merge request > Context commits', :js, feature_category: :code_review do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb
index 8af0e957c14..f2ec0e2df6d 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User posts diff notes', :js do
+RSpec.describe 'Merge request > User posts diff notes', :js, feature_category: :code_review do
include MergeRequestDiffHelpers
include Spec::Support::Helpers::ModalHelpers
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index 844ef6133c8..194e04a9544 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User posts notes', :js do
+RSpec.describe 'Merge request > User posts notes', :js, feature_category: :code_review do
include NoteInteractionHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/features/merge_request/user_rebases_merge_request_spec.rb b/spec/features/merge_request/user_rebases_merge_request_spec.rb
index d42864200ec..c3ee5ddc3b1 100644
--- a/spec/features/merge_request/user_rebases_merge_request_spec.rb
+++ b/spec/features/merge_request/user_rebases_merge_request_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User rebases a merge request", :js do
+RSpec.describe "User rebases a merge request", :js, feature_category: :code_review do
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:user) { project.first_owner }
diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb
index a04ca4e789c..d4c80c1e9e2 100644
--- a/spec/features/merge_request/user_resolves_conflicts_spec.rb
+++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User resolves conflicts', :js do
+RSpec.describe 'Merge request > User resolves conflicts', :js, feature_category: :code_review do
include Spec::Support::Helpers::Features::SourceEditorSpecHelpers
let(:project) { create(:project, :repository) }
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index 99f1b1ab1ad..f0507e94424 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
+RSpec.describe 'Merge request > User resolves diff notes and threads', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:guest) { create(:user) }
diff --git a/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb b/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb
index f8f3467f6fb..a7508ede1a1 100644
--- a/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb
+++ b/spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User resolves outdated diff discussions', :js do
+RSpec.describe 'Merge request > User resolves outdated diff discussions', :js, feature_category: :code_review do
let(:project) { create(:project, :repository, :public) }
let(:merge_request) do
diff --git a/spec/features/merge_request/user_resolves_wip_mr_spec.rb b/spec/features/merge_request/user_resolves_wip_mr_spec.rb
index 92927b713f1..b7f20a16a3f 100644
--- a/spec/features/merge_request/user_resolves_wip_mr_spec.rb
+++ b/spec/features/merge_request/user_resolves_wip_mr_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User resolves Draft', :js do
+RSpec.describe 'Merge request > User resolves Draft', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) do
diff --git a/spec/features/merge_request/user_reverts_merge_request_spec.rb b/spec/features/merge_request/user_reverts_merge_request_spec.rb
index 9cbba6c470f..edfa9267871 100644
--- a/spec/features/merge_request/user_reverts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_reverts_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User reverts a merge request', :js do
+RSpec.describe 'User reverts a merge request', :js, feature_category: :code_review do
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/merge_request/user_reviews_image_spec.rb b/spec/features/merge_request/user_reviews_image_spec.rb
index bd490294829..5814dc6b58c 100644
--- a/spec/features/merge_request/user_reviews_image_spec.rb
+++ b/spec/features/merge_request/user_reviews_image_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > image review', :js do
+RSpec.describe 'Merge request > image review', :js, feature_category: :code_review do
include MergeRequestDiffHelpers
include RepoHelpers
diff --git a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
index cf4875a7a25..fdd2aeec274 100644
--- a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
+++ b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User scrolls to note on load', :js do
+RSpec.describe 'Merge request > User scrolls to note on load', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project, author: user) }
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index a6c024be698..8c2fc62d16f 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
include Spec::Support::Helpers::ModalHelpers # rubocop:disable Style/MixinUsage
-RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
+RSpec.describe 'Merge request > User sees avatars on diff notes', :js, feature_category: :code_review do
include NoteInteractionHelpers
include Spec::Support::Helpers::ModalHelpers
include MergeRequestDiffHelpers
diff --git a/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb b/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb
index d8b258bac47..0b6aefcdab6 100644
--- a/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb
+++ b/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'New merge request breadcrumb' do
+RSpec.describe 'New merge request breadcrumb', feature_category: :code_review do
let(:project) { create(:project, :repository) }
let(:user) { project.creator }
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 5827266d4b7..bbfa2be47cc 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees check out branch modal', :js do
+RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb b/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb
index 35be21a646e..07b7cb1e8d8 100644
--- a/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb
+++ b/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User cherry-picks', :js do
+RSpec.describe 'Merge request > User cherry-picks', :js, feature_category: :code_review do
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: group) }
let(:user) { project.creator }
diff --git a/spec/features/merge_request/user_sees_closing_issues_message_spec.rb b/spec/features/merge_request/user_sees_closing_issues_message_spec.rb
index f56db3d3dbe..9a1d47a13b5 100644
--- a/spec/features/merge_request/user_sees_closing_issues_message_spec.rb
+++ b/spec/features/merge_request/user_sees_closing_issues_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees closing issues message', :js do
+RSpec.describe 'Merge request > User sees closing issues message', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:issue_1) { create(:issue, project: project) }
diff --git a/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb b/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb
index dc50c3bc8db..16ae8b4304b 100644
--- a/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb
+++ b/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees deleted target branch', :js do
+RSpec.describe 'Merge request > User sees deleted target branch', :js, feature_category: :code_review do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:user) { project.creator }
diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
index 6f8ecf5f5c2..40ab06937ff 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees deployment widget', :js do
+RSpec.describe 'Merge request > User sees deployment widget', :js, feature_category: :continuous_delivery do
include Spec::Support::Helpers::ModalHelpers
describe 'when merge request has associated environments' do
diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb
index 0bae019793c..101ff8fc152 100644
--- a/spec/features/merge_request/user_sees_diff_spec.rb
+++ b/spec/features/merge_request/user_sees_diff_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees diff', :js do
+RSpec.describe 'Merge request > User sees diff', :js, feature_category: :code_review do
include ProjectForksHelper
include RepoHelpers
include MergeRequestDiffHelpers
diff --git a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
index 9fbe7662fc0..a22fb2cff00 100644
--- a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees discussions navigation', :js do
+RSpec.describe 'Merge request > User sees discussions navigation', :js, feature_category: :code_review do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:user) { project.creator }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb
index cc477e363a4..0eae6e39eec 100644
--- a/spec/features/merge_request/user_sees_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees threads', :js do
+RSpec.describe 'Merge request > User sees threads', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
index e250837f398..6db5480abb4 100644
--- a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees merge button depending on unresolved threads', :js do
+RSpec.describe 'Merge request > User sees merge button depending on unresolved threads', :js,
+feature_category: :code_review do
let(:project) { create(:project, :repository) }
let(:user) { project.creator }
let!(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index 2a1b9ea6009..f7594c717d1 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees pipelines triggered by merge request', :js do
+RSpec.describe 'Merge request > User sees pipelines triggered by merge request', :js, feature_category: :code_review do
include ProjectForksHelper
include TestReportsHelper
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 c4a29c1fb07..e5aa0f6e64d 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees merge widget', :js do
+RSpec.describe 'Merge request > User sees merge widget', :js, feature_category: :code_review do
include ProjectForksHelper
include TestReportsHelper
include ReactiveCachingHelpers
diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
index 03f9f6ef565..5756218d20f 100644
--- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request < User sees mini pipeline graph', :js do
+RSpec.describe 'Merge request < User sees mini pipeline graph', :js, feature_category: :continuous_integration do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project, head_pipeline: pipeline) }
diff --git a/spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb b/spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb
index a764dd97878..4bfdce29c6a 100644
--- a/spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb
+++ b/spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees MR from deleted forked project', :js do
+RSpec.describe 'Merge request > User sees MR from deleted forked project', :js, feature_category: :code_review do
include ProjectForksHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
index 39bba3f2f73..8e6f6d04676 100644
--- a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
+++ b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
# This test serves as a regression test for a bug that caused an error
# message to be shown by JavaScript when the source branch was deleted.
# Please do not remove ":js".
-RSpec.describe 'Merge request > User sees MR with deleted source branch', :js do
+RSpec.describe 'Merge request > User sees MR with deleted source branch', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:user) { project.creator }
diff --git a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
index b8b7fc2009f..8f011f5616b 100644
--- a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
+++ b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees notes from forked project', :js do
+RSpec.describe 'Merge request > User sees notes from forked project', :js, feature_category: :code_review do
include ProjectForksHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_request/user_sees_page_metadata_spec.rb b/spec/features/merge_request/user_sees_page_metadata_spec.rb
index 7b3e07152a0..f97732f91a7 100644
--- a/spec/features/merge_request/user_sees_page_metadata_spec.rb
+++ b/spec/features/merge_request/user_sees_page_metadata_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees page metadata' do
+RSpec.describe 'Merge request > User sees page metadata', feature_category: :code_review do
let(:merge_request) { create(:merge_request, description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') }
let(:project) { merge_request.target_project }
let(:user) { project.creator }
diff --git a/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb b/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb
index a9fefc89d6c..0816b14f9a5 100644
--- a/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees pipelines from forked project', :js do
+RSpec.describe 'Merge request > User sees pipelines from forked project', :js,
+feature_category: :continuous_integration do
include ProjectForksHelper
let(:target_project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index 11e542916f9..8faaf6bf39b 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees pipelines', :js do
+RSpec.describe 'Merge request > User sees pipelines', :js, feature_category: :code_review do
describe 'pipeline tab' do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.target_project }
@@ -35,7 +35,7 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
end
wait_for_requests
- expect(page).to have_selector('.stage-cell')
+ expect(page).to have_css('[data-testid="pipeline-mini-graph"]')
end
context 'with a detached merge request pipeline' do
diff --git a/spec/features/merge_request/user_sees_suggest_pipeline_spec.rb b/spec/features/merge_request/user_sees_suggest_pipeline_spec.rb
index 448ef750508..3a2e382fe99 100644
--- a/spec/features/merge_request/user_sees_suggest_pipeline_spec.rb
+++ b/spec/features/merge_request/user_sees_suggest_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees suggest pipeline', :js do
+RSpec.describe 'Merge request > User sees suggest pipeline', :js, feature_category: :continuous_integration do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.source_project }
let(:user) { project.creator }
diff --git a/spec/features/merge_request/user_sees_system_notes_spec.rb b/spec/features/merge_request/user_sees_system_notes_spec.rb
index 9f8d4c6d63f..40402c95d6f 100644
--- a/spec/features/merge_request/user_sees_system_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_system_notes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees system notes', :js do
+RSpec.describe 'Merge request > User sees system notes', :js, feature_category: :code_review do
let(:public_project) { create(:project, :public, :repository) }
let(:private_project) { create(:project, :private, :repository) }
let(:user) { private_project.creator }
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index 0e86e970f46..f0ff6e1769a 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees versions', :js do
+RSpec.describe 'Merge request > User sees versions', :js, feature_category: :code_review do
include MergeRequestDiffHelpers
let(:merge_request) do
diff --git a/spec/features/merge_request/user_sees_wip_help_message_spec.rb b/spec/features/merge_request/user_sees_wip_help_message_spec.rb
index d33e54f2e3d..1a751af6ded 100644
--- a/spec/features/merge_request/user_sees_wip_help_message_spec.rb
+++ b/spec/features/merge_request/user_sees_wip_help_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees draft help message' do
+RSpec.describe 'Merge request > User sees draft help message', feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
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 bcc6abd4f65..8b6c9dc18f6 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User selects branches for new MR', :js do
+RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
diff --git a/spec/features/merge_request/user_squashes_merge_request_spec.rb b/spec/features/merge_request/user_squashes_merge_request_spec.rb
index da0d4ca23d1..43590aed3cc 100644
--- a/spec/features/merge_request/user_squashes_merge_request_spec.rb
+++ b/spec/features/merge_request/user_squashes_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User squashes a merge request', :js do
+RSpec.describe 'User squashes a merge request', :js, feature_category: :code_review do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:source_branch) { 'csv' }
diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
index f77a42ee506..5a5494a2fe9 100644
--- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
+++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User comments on a diff', :js do
+RSpec.describe 'User comments on a diff', :js, feature_category: :code_review do
include MergeRequestDiffHelpers
include RepoHelpers
diff --git a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
index 19774accaaf..993eb59cb74 100644
--- a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
+++ b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User toggles whitespace changes', :js do
+RSpec.describe 'Merge request > User toggles whitespace changes', :js, feature_category: :code_review do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:user) { project.creator }
diff --git a/spec/features/merge_request/user_tries_to_access_private_project_info_through_new_mr_spec.rb b/spec/features/merge_request/user_tries_to_access_private_project_info_through_new_mr_spec.rb
index 96a1cd81c93..5095457509a 100644
--- a/spec/features/merge_request/user_tries_to_access_private_project_info_through_new_mr_spec.rb
+++ b/spec/features/merge_request/user_tries_to_access_private_project_info_through_new_mr_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Merge Request > User tries to access private project information through the new mr page' do
+RSpec.describe 'Merge Request > User tries to access private project information through the new mr page',
+feature_category: :code_review do
let(:current_user) { create(:user) }
let(:private_project) do
create(:project, :public, :repository,
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 ca102913369..99befbace74 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -7,7 +7,8 @@ require 'spec_helper'
# for example, adding quick actions when creating the issue and checking DateTime formats on UI.
# Because this kind of spec takes more time to run there is no need to add new ones
# for each existing quick action unless they test something not tested by existing tests.
-RSpec.describe 'Merge request > User uses quick actions', :js, :use_clean_rails_redis_caching do
+RSpec.describe 'Merge request > User uses quick actions', :js, :use_clean_rails_redis_caching,
+feature_category: :code_review do
include Spec::Support::Helpers::Features::NotesHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_request/user_views_auto_expanding_diff_spec.rb b/spec/features/merge_request/user_views_auto_expanding_diff_spec.rb
index 1748f66c934..19a77a9192c 100644
--- a/spec/features/merge_request/user_views_auto_expanding_diff_spec.rb
+++ b/spec/features/merge_request/user_views_auto_expanding_diff_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views diffs file-by-file', :js do
+RSpec.describe 'User views diffs file-by-file', :js, feature_category: :code_review do
let(:merge_request) do
create(:merge_request, source_branch: 'squash-large-files', source_project: project, target_project: project)
end
diff --git a/spec/features/merge_request/user_views_diffs_commit_spec.rb b/spec/features/merge_request/user_views_diffs_commit_spec.rb
index cf92603972e..84cbfb35539 100644
--- a/spec/features/merge_request/user_views_diffs_commit_spec.rb
+++ b/spec/features/merge_request/user_views_diffs_commit_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views diff by commit', :js do
+RSpec.describe 'User views diff by commit', :js, feature_category: :code_review do
let(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
end
diff --git a/spec/features/merge_request/user_views_diffs_file_by_file_spec.rb b/spec/features/merge_request/user_views_diffs_file_by_file_spec.rb
index ad9c342df3e..9db6f86e14d 100644
--- a/spec/features/merge_request/user_views_diffs_file_by_file_spec.rb
+++ b/spec/features/merge_request/user_views_diffs_file_by_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views diffs file-by-file', :js do
+RSpec.describe 'User views diffs file-by-file', :js, feature_category: :code_review do
let(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
end
diff --git a/spec/features/merge_request/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb
index 894292c99eb..7363f6dfb32 100644
--- a/spec/features/merge_request/user_views_diffs_spec.rb
+++ b/spec/features/merge_request/user_views_diffs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views diffs', :js do
+RSpec.describe 'User views diffs', :js, feature_category: :code_review do
let(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
end
diff --git a/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb b/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb
index e3272a6e280..2a9275adfcf 100644
--- a/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb
+++ b/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb
@@ -6,7 +6,7 @@ require 'spec_helper'
# updated.
# This can occur when the fork a merge request is created from is in the process
# of being destroyed.
-RSpec.describe 'User views merged merge request from deleted fork' do
+RSpec.describe 'User views merged merge request from deleted fork', feature_category: :code_review do
include ProjectForksHelper
let(:project) { create(:project, :repository) }
diff --git a/spec/features/merge_request/user_views_open_merge_request_spec.rb b/spec/features/merge_request/user_views_open_merge_request_spec.rb
index 1f4682b4a46..8b9e973217d 100644
--- a/spec/features/merge_request/user_views_open_merge_request_spec.rb
+++ b/spec/features/merge_request/user_views_open_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views an open merge request' do
+RSpec.describe 'User views an open merge request', feature_category: :code_review do
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project, description: '# Description header')
end
diff --git a/spec/features/merge_requests/filters_generic_behavior_spec.rb b/spec/features/merge_requests/filters_generic_behavior_spec.rb
index 80009cca2fb..0d6b5edcbab 100644
--- a/spec/features/merge_requests/filters_generic_behavior_spec.rb
+++ b/spec/features/merge_requests/filters_generic_behavior_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Requests > Filters generic behavior', :js do
+RSpec.describe 'Merge Requests > Filters generic behavior', :js, feature_category: :code_review do
include FilteredSearchHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/rss_spec.rb b/spec/features/merge_requests/rss_spec.rb
index 9fc3d3d6ae1..4c73ce3b684 100644
--- a/spec/features/merge_requests/rss_spec.rb
+++ b/spec/features/merge_requests/rss_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Merge Requests RSS' do
+RSpec.describe 'Project Merge Requests RSS', feature_category: :code_review do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
diff --git a/spec/features/merge_requests/user_exports_as_csv_spec.rb b/spec/features/merge_requests/user_exports_as_csv_spec.rb
index 351e714b612..aedd7ef4d79 100644
--- a/spec/features/merge_requests/user_exports_as_csv_spec.rb
+++ b/spec/features/merge_requests/user_exports_as_csv_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Requests > Exports as CSV', :js do
+RSpec.describe 'Merge Requests > Exports as CSV', :js, feature_category: :code_review do
let!(:project) { create(:project, :public, :repository) }
let!(:user) { project.creator }
let!(:open_mr) { create(:merge_request, title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1') }
diff --git a/spec/features/merge_requests/user_filters_by_approvals_spec.rb b/spec/features/merge_requests/user_filters_by_approvals_spec.rb
index 6dda9ca7952..56c8a65385c 100644
--- a/spec/features/merge_requests/user_filters_by_approvals_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_approvals_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Requests > User filters', :js do
+RSpec.describe 'Merge Requests > User filters', :js, feature_category: :code_review do
include FilteredSearchHelpers
let_it_be(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/user_filters_by_assignees_spec.rb b/spec/features/merge_requests/user_filters_by_assignees_spec.rb
index 9827b067649..818cf6f076f 100644
--- a/spec/features/merge_requests/user_filters_by_assignees_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_assignees_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Requests > User filters by assignees', :js do
+RSpec.describe 'Merge Requests > User filters by assignees', :js, feature_category: :code_review do
include FilteredSearchHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/user_filters_by_deployments_spec.rb b/spec/features/merge_requests/user_filters_by_deployments_spec.rb
index 157454d4e10..5f7d2fa9f9a 100644
--- a/spec/features/merge_requests/user_filters_by_deployments_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_deployments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Requests > User filters by deployments', :js do
+RSpec.describe 'Merge Requests > User filters by deployments', :js, feature_category: :code_review do
include FilteredSearchHelpers
let!(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/user_filters_by_draft_spec.rb b/spec/features/merge_requests/user_filters_by_draft_spec.rb
index de070805d96..d50d7edaefb 100644
--- a/spec/features/merge_requests/user_filters_by_draft_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_draft_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Requests > User filters by draft', :js do
+RSpec.describe 'Merge Requests > User filters by draft', :js, feature_category: :code_review do
include FilteredSearchHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/user_filters_by_labels_spec.rb b/spec/features/merge_requests/user_filters_by_labels_spec.rb
index 980046ccd71..030eb1b6431 100644
--- a/spec/features/merge_requests/user_filters_by_labels_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_labels_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Requests > User filters by labels', :js do
+RSpec.describe 'Merge Requests > User filters by labels', :js, feature_category: :code_review do
include FilteredSearchHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/user_filters_by_milestones_spec.rb b/spec/features/merge_requests/user_filters_by_milestones_spec.rb
index 877d5e6a4ee..abdb6c7787b 100644
--- a/spec/features/merge_requests/user_filters_by_milestones_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_milestones_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Requests > User filters by milestones', :js do
+RSpec.describe 'Merge Requests > User filters by milestones', :js, feature_category: :code_review do
include FilteredSearchHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
index 3aba023b077..ae171f47ec3 100644
--- a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge requests > User filters by multiple criteria', :js do
+RSpec.describe 'Merge requests > User filters by multiple criteria', :js, feature_category: :code_review do
include FilteredSearchHelpers
let!(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/user_filters_by_target_branch_spec.rb b/spec/features/merge_requests/user_filters_by_target_branch_spec.rb
index 1d9c80238f5..e0755695f5c 100644
--- a/spec/features/merge_requests/user_filters_by_target_branch_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_target_branch_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Requests > User filters by target branch', :js do
+RSpec.describe 'Merge Requests > User filters by target branch', :js, feature_category: :code_review do
include FilteredSearchHelpers
let!(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
index 2743f7e8ed4..d9c3bcda0d3 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge requests > User lists merge requests' do
+RSpec.describe 'Merge requests > User lists merge requests', feature_category: :code_review do
include MergeRequestHelpers
include SortingHelper
diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb
index 5c3cb098e28..133017d5b25 100644
--- a/spec/features/merge_requests/user_mass_updates_spec.rb
+++ b/spec/features/merge_requests/user_mass_updates_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge requests > User mass updates', :js do
+RSpec.describe 'Merge requests > User mass updates', :js, feature_category: :code_review do
let(:project) { create(:project, :repository) }
let(:user) { project.creator }
let(:user2) { create(:user) }
diff --git a/spec/features/merge_requests/user_sees_empty_state_spec.rb b/spec/features/merge_requests/user_sees_empty_state_spec.rb
index 056da53c47b..a50ea300249 100644
--- a/spec/features/merge_requests/user_sees_empty_state_spec.rb
+++ b/spec/features/merge_requests/user_sees_empty_state_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees empty state' do
+RSpec.describe 'Merge request > User sees empty state', feature_category: :code_review do
include ProjectForksHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
index 4a124299c61..d268cfc59f3 100644
--- a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User sorts merge requests', :js do
+RSpec.describe 'User sorts merge requests', :js, feature_category: :code_review do
include CookieHelper
include Spec::Support::Helpers::Features::SortingHelpers
diff --git a/spec/features/merge_requests/user_views_all_merge_requests_spec.rb b/spec/features/merge_requests/user_views_all_merge_requests_spec.rb
index f8fe2c5ebe2..b55e4bd153f 100644
--- a/spec/features/merge_requests/user_views_all_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_views_all_merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views all merge requests' do
+RSpec.describe 'User views all merge requests', feature_category: :code_review do
let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/merge_requests/user_views_closed_merge_requests_spec.rb b/spec/features/merge_requests/user_views_closed_merge_requests_spec.rb
index abc652c3bbd..4c2598dcc9c 100644
--- a/spec/features/merge_requests/user_views_closed_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_views_closed_merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views closed merge requests' do
+RSpec.describe 'User views closed merge requests', feature_category: :code_review do
let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/merge_requests/user_views_merged_merge_requests_spec.rb b/spec/features/merge_requests/user_views_merged_merge_requests_spec.rb
index 3b93fb7e4bf..2526f1a855b 100644
--- a/spec/features/merge_requests/user_views_merged_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_views_merged_merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views merged merge requests' do
+RSpec.describe 'User views merged merge requests', feature_category: :code_review do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let!(:merged_merge_request) { create(:merged_merge_request, source_project: project, target_project: project) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/merge_requests/user_views_open_merge_requests_spec.rb b/spec/features/merge_requests/user_views_open_merge_requests_spec.rb
index 49509f89a8d..3c53bc5e283 100644
--- a/spec/features/merge_requests/user_views_open_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_views_open_merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views open merge requests' do
+RSpec.describe 'User views open merge requests', feature_category: :code_review do
let_it_be(:user) { create(:user) }
shared_examples_for 'shows merge requests' do
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index 98d623902a5..50cd6b9e801 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Milestone' do
+RSpec.describe 'Milestone', feature_category: :team_planning do
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, namespace: group) }
let(:user) { create(:user) }
diff --git a/spec/features/milestones/user_creates_milestone_spec.rb b/spec/features/milestones/user_creates_milestone_spec.rb
index 1ab231632fb..b750f035e36 100644
--- a/spec/features/milestones/user_creates_milestone_spec.rb
+++ b/spec/features/milestones/user_creates_milestone_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "User creates milestone", :js do
+RSpec.describe "User creates milestone", :js, feature_category: :team_planning do
let_it_be(:developer) { create(:user) }
let_it_be(:inherited_guest) { create(:user) }
let_it_be(:inherited_developer) { create(:user) }
diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb
index 40626407642..141e626c6f3 100644
--- a/spec/features/milestones/user_deletes_milestone_spec.rb
+++ b/spec/features/milestones/user_deletes_milestone_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "User deletes milestone", :js do
+RSpec.describe "User deletes milestone", :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
diff --git a/spec/features/milestones/user_edits_milestone_spec.rb b/spec/features/milestones/user_edits_milestone_spec.rb
index 3edd50922b6..438ca6a2518 100644
--- a/spec/features/milestones/user_edits_milestone_spec.rb
+++ b/spec/features/milestones/user_edits_milestone_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "User edits milestone", :js do
+RSpec.describe "User edits milestone", :js, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project, start_date: Date.today, due_date: 5.days.from_now) }
diff --git a/spec/features/milestones/user_promotes_milestone_spec.rb b/spec/features/milestones/user_promotes_milestone_spec.rb
index a9c3c9706a0..0eacf36cdde 100644
--- a/spec/features/milestones/user_promotes_milestone_spec.rb
+++ b/spec/features/milestones/user_promotes_milestone_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User promotes milestone' do
+RSpec.describe 'User promotes milestone', feature_category: :team_planning do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: group) }
diff --git a/spec/features/milestones/user_sees_breadcrumb_links_spec.rb b/spec/features/milestones/user_sees_breadcrumb_links_spec.rb
index e9cfa9b20dc..febe803426a 100644
--- a/spec/features/milestones/user_sees_breadcrumb_links_spec.rb
+++ b/spec/features/milestones/user_sees_breadcrumb_links_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'New project milestone breadcrumb' do
+RSpec.describe 'New project milestone breadcrumb', feature_category: :team_planning do
let(:project) { create(:project) }
let(:milestone) { create(:milestone, project: project) }
let(:user) { project.creator }
diff --git a/spec/features/milestones/user_views_milestone_spec.rb b/spec/features/milestones/user_views_milestone_spec.rb
index 8674d59afdf..6c8dc47cce1 100644
--- a/spec/features/milestones/user_views_milestone_spec.rb
+++ b/spec/features/milestones/user_views_milestone_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "User views milestone" do
+RSpec.describe "User views milestone", feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
diff --git a/spec/features/milestones/user_views_milestones_spec.rb b/spec/features/milestones/user_views_milestones_spec.rb
index 752cc63486f..81823d6ba30 100644
--- a/spec/features/milestones/user_views_milestones_spec.rb
+++ b/spec/features/milestones/user_views_milestones_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "User views milestones" do
+RSpec.describe "User views milestones", feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project) }
diff --git a/spec/features/monitor_sidebar_link_spec.rb b/spec/features/monitor_sidebar_link_spec.rb
index 4f529179522..d5f987d15c2 100644
--- a/spec/features/monitor_sidebar_link_spec.rb
+++ b/spec/features/monitor_sidebar_link_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
+RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category: :not_owned do
let_it_be_with_reload(:project) { create(:project, :internal, :repository) }
let_it_be(:user) { create(:user) }
@@ -19,21 +19,13 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
let(:enabled) { Featurable::PRIVATE }
let(:disabled) { Featurable::DISABLED }
- where(:flag_enabled, :operations_access_level, :monitor_level, :render) do
- true | ref(:disabled) | ref(:enabled) | true
- true | ref(:disabled) | ref(:disabled) | false
- true | ref(:enabled) | ref(:enabled) | true
- true | ref(:enabled) | ref(:disabled) | false
- false | ref(:disabled) | ref(:enabled) | false
- false | ref(:disabled) | ref(:disabled) | false
- false | ref(:enabled) | ref(:enabled) | true
- false | ref(:enabled) | ref(:disabled) | true
+ where(:monitor_level, :render) do
+ ref(:enabled) | true
+ ref(:disabled) | false
end
with_them do
it 'renders when expected to' do
- stub_feature_flags(split_operations_visibility_permissions: flag_enabled)
- project.project_feature.update_attribute(:operations_access_level, operations_access_level)
project.project_feature.update_attribute(:monitor_access_level, monitor_level)
visit project_issues_path(project)
@@ -51,7 +43,6 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
let(:access_level) { ProjectFeature::PUBLIC }
before do
- project.project_feature.update_attribute(:operations_access_level, access_level)
project.project_feature.update_attribute(:monitor_access_level, access_level)
end
@@ -67,41 +58,19 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
end
- context 'with new monitor visiblity flag disabled' do
- stub_feature_flags(split_operations_visibility_permissions: false)
+ context 'when monitor project feature is PRIVATE' do
+ let(:access_level) { ProjectFeature::PRIVATE }
- context 'when operations 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')
- end
- end
-
- context 'when operations project feature is DISABLED' do
- let(:access_level) { ProjectFeature::DISABLED }
-
- it 'does not show the `Operations` menu' do
- expect(page).not_to have_selector('a.shortcuts-monitor')
- end
+ it 'does not show the `Monitor` menu' do
+ expect(page).not_to have_selector('a.shortcuts-monitor')
end
end
- context 'with new monitor visiblity flag enabled' do
- context 'when monitor project feature is PRIVATE' do
- let(:access_level) { ProjectFeature::PRIVATE }
+ 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')
- end
- end
-
- context 'when operations project feature is DISABLED' do
- let(:access_level) { ProjectFeature::DISABLED }
-
- it 'does not show the `Operations` menu' do
- expect(page).not_to have_selector('a.shortcuts-monitor')
- end
+ it 'does not show the `Monitor` menu' do
+ expect(page).not_to have_selector('a.shortcuts-monitor')
end
end
end
diff --git a/spec/features/nav/new_nav_toggle_spec.rb b/spec/features/nav/new_nav_toggle_spec.rb
new file mode 100644
index 00000000000..f040d801cfb
--- /dev/null
+++ b/spec/features/nav/new_nav_toggle_spec.rb
@@ -0,0 +1,71 @@
+# 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)
+ stub_feature_flags(super_sidebar_nav: new_nav_ff)
+ sign_in(user)
+ visit explore_projects_path
+ end
+
+ context 'with feature flag off' do
+ let(:new_nav_ff) { false }
+
+ where(:user_preference) do
+ [true, false]
+ end
+
+ with_them do
+ it 'shows old topbar user dropdown with no way to toggle to new nav' do
+ within '.js-header-content .js-nav-user-dropdown' do
+ find('a[data-toggle="dropdown"]').click
+ expect(page).not_to have_content('Navigation redesign')
+ end
+ end
+ end
+ end
+
+ context 'with feature flag on' do
+ let(:new_nav_ff) { true }
+
+ 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
+ end
+
+ context 'when user has new nav enabled' do
+ let(:user_preference) { true }
+
+ it 'allows to disable 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.is-checked')
+ toggle.click # reloads the page
+ end
+
+ wait_for_requests
+
+ expect(user.reload.use_new_navigation).to eq false
+ end
+ end
+ end
+end
diff --git a/spec/features/nav/top_nav_responsive_spec.rb b/spec/features/nav/top_nav_responsive_spec.rb
index 4f8e47b5068..d038c5d9e32 100644
--- a/spec/features/nav/top_nav_responsive_spec.rb
+++ b/spec/features/nav/top_nav_responsive_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'top nav responsive', :js do
+RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
include MobileHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/nav/top_nav_tooltip_spec.rb b/spec/features/nav/top_nav_tooltip_spec.rb
index a110c6cfecf..17828778112 100644
--- a/spec/features/nav/top_nav_tooltip_spec.rb
+++ b/spec/features/nav/top_nav_tooltip_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'top nav tooltips', :js do
+RSpec.describe 'top nav tooltips', :js, feature_category: :navigation do
let_it_be(:user) { create(:user) }
before do
diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb
index fca8972b56c..07d0fca0139 100644
--- a/spec/features/oauth_login_spec.rb
+++ b/spec/features/oauth_login_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OAuth Login', :allow_forgery_protection do
+RSpec.describe 'OAuth Login', :allow_forgery_protection, feature_category: :system_access do
include DeviseHelpers
def enter_code(code)
diff --git a/spec/features/oauth_provider_authorize_spec.rb b/spec/features/oauth_provider_authorize_spec.rb
index f5a1a35b66f..7638563b4a3 100644
--- a/spec/features/oauth_provider_authorize_spec.rb
+++ b/spec/features/oauth_provider_authorize_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OAuth Provider' do
+RSpec.describe 'OAuth Provider', feature_category: :system_access do
describe 'Standard OAuth Authorization' do
let(:application) { create(:oauth_application, scopes: 'read_user') }
diff --git a/spec/features/oauth_registration_spec.rb b/spec/features/oauth_registration_spec.rb
index 0a35b5a7e42..48996164bd3 100644
--- a/spec/features/oauth_registration_spec.rb
+++ b/spec/features/oauth_registration_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OAuth Registration', :js, :allow_forgery_protection do
+RSpec.describe 'OAuth Registration', :js, :allow_forgery_protection, feature_category: :system_access do
include LoginHelpers
include TermsHelper
using RSpec::Parameterized::TableSyntax
diff --git a/spec/features/one_trust_spec.rb b/spec/features/one_trust_spec.rb
index a7dfbfd6bdf..2aeb722886e 100644
--- a/spec/features/one_trust_spec.rb
+++ b/spec/features/one_trust_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OneTrust' do
+RSpec.describe 'OneTrust', feature_category: :system_access do
context 'almost there page' do
context 'when OneTrust is enabled' do
let_it_be(:onetrust_url) { 'https://*.onetrust.com' }
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index b2739454b52..272365ac7ee 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Member autocomplete', :js do
+RSpec.describe 'Member autocomplete', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:author) { create(:user) }
diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb
index f89e19f5361..7d36e905327 100644
--- a/spec/features/password_reset_spec.rb
+++ b/spec/features/password_reset_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Password reset' do
+RSpec.describe 'Password reset', feature_category: :system_access do
describe 'throttling' do
it 'sends reset instructions when not previously sent' do
user = create(:user)
diff --git a/spec/features/populate_new_pipeline_vars_with_params_spec.rb b/spec/features/populate_new_pipeline_vars_with_params_spec.rb
index 75fa8561235..a83b5a81a41 100644
--- a/spec/features/populate_new_pipeline_vars_with_params_spec.rb
+++ b/spec/features/populate_new_pipeline_vars_with_params_spec.rb
@@ -2,47 +2,29 @@
require 'spec_helper'
-RSpec.describe "Populate new pipeline CI variables with url params", :js do
+RSpec.describe "Populate new pipeline CI variables with url params", :js, feature_category: :pipeline_authoring do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:page_path) { new_project_pipeline_path(project) }
- shared_examples 'form pre-filled with URL params' do
- before do
- sign_in(user)
- project.add_maintainer(user)
+ before do
+ sign_in(user)
+ project.add_maintainer(user)
- visit "#{page_path}?var[key1]=value1&file_var[key2]=value2"
- end
-
- it "var[key1]=value1 populates env_var variable correctly" do
- page.within(all("[data-testid='ci-variable-row']")[0]) do
- expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key1')
- expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value1')
- end
- end
-
- it "file_var[key2]=value2 populates file variable correctly" do
- page.within(all("[data-testid='ci-variable-row']")[1]) do
- expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key2')
- expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value2')
- end
- end
+ visit "#{page_path}?var[key1]=value1&file_var[key2]=value2"
end
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(run_pipeline_graphql: false)
+ it "var[key1]=value1 populates env_var variable correctly" do
+ page.within(all("[data-testid='ci-variable-row']")[0]) do
+ expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key1')
+ expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value1')
end
-
- it_behaves_like 'form pre-filled with URL params'
end
- context 'when feature flag is enabled' do
- before do
- stub_feature_flags(run_pipeline_graphql: true)
+ it "file_var[key2]=value2 populates file variable correctly" do
+ page.within(all("[data-testid='ci-variable-row']")[1]) do
+ expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key2')
+ expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value2')
end
-
- it_behaves_like 'form pre-filled with URL params'
end
end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index ca156642bc8..87a65438768 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Profile account page', :js do
+RSpec.describe 'Profile account page', :js, feature_category: :users do
include Spec::Support::Helpers::ModalHelpers
let(:user) { create(:user) }
diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb
index 4fe0c3d035e..82c45862e07 100644
--- a/spec/features/profiles/account_spec.rb
+++ b/spec/features/profiles/account_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Profile > Account', :js do
+RSpec.describe 'Profile > Account', :js, feature_category: :users do
let(:user) { create(:user, username: 'foo') }
before do
diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb
index d0819bb5363..5c20735cf35 100644
--- a/spec/features/profiles/active_sessions_spec.rb
+++ b/spec/features/profiles/active_sessions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
+RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, feature_category: :users do
include Spec::Support::Helpers::ModalHelpers
let(:user) do
diff --git a/spec/features/profiles/chat_names_spec.rb b/spec/features/profiles/chat_names_spec.rb
index 82134de582a..b3d65ab3a3c 100644
--- a/spec/features/profiles/chat_names_spec.rb
+++ b/spec/features/profiles/chat_names_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Profile > Chat' do
+RSpec.describe 'Profile > Chat', feature_category: :users do
let(:user) { create(:user) }
let(:integration) { create(:integration) }
diff --git a/spec/features/profiles/emails_spec.rb b/spec/features/profiles/emails_spec.rb
index 24917412826..e8ea227c072 100644
--- a/spec/features/profiles/emails_spec.rb
+++ b/spec/features/profiles/emails_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Profile > Emails' do
+RSpec.describe 'Profile > Emails', feature_category: :users do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb
index 4eedeeac262..1d014f983e7 100644
--- a/spec/features/profiles/gpg_keys_spec.rb
+++ b/spec/features/profiles/gpg_keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Profile > GPG Keys' do
+RSpec.describe 'Profile > GPG Keys', feature_category: :users do
let(:user) { create(:user, email: GpgHelpers::User2.emails.first) }
before do
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index 65944f5a537..7a2a12d8dca 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Profile > SSH Keys' do
+RSpec.describe 'Profile > SSH Keys', feature_category: :users do
let(:user) { create(:user) }
before do
@@ -80,7 +80,7 @@ RSpec.describe 'Profile > SSH Keys' do
shared_examples 'removes key' do
it 'removes key' do
visit path
- click_button('Delete')
+ find('[data-testid=remove-icon]').click
page.within('.modal') do
page.click_button('Delete')
diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb
index 7d8cd2dc6ca..80d05fd5cc7 100644
--- a/spec/features/profiles/oauth_applications_spec.rb
+++ b/spec/features/profiles/oauth_applications_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Profile > Applications' do
+RSpec.describe 'Profile > Applications', feature_category: :users do
include Spec::Support::Helpers::ModalHelpers
let(:user) { create(:user) }
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index 8887ff1746d..b324ee17873 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Profile > Password' do
+RSpec.describe 'Profile > Password', feature_category: :users do
let(:user) { create(:user) }
def fill_passwords(password, confirmation)
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index 3ae88da06f6..3087d7ff296 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Profile > Personal Access Tokens', :js do
+RSpec.describe 'Profile > Personal Access Tokens', :js, feature_category: :users do
include Spec::Support::Helpers::ModalHelpers
include Spec::Support::Helpers::AccessTokenHelpers
diff --git a/spec/features/profiles/two_factor_auths_spec.rb b/spec/features/profiles/two_factor_auths_spec.rb
index decc2904b6e..8dddaad11c3 100644
--- a/spec/features/profiles/two_factor_auths_spec.rb
+++ b/spec/features/profiles/two_factor_auths_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Two factor auths' do
+RSpec.describe 'Two factor auths', feature_category: :users do
include Spec::Support::Helpers::ModalHelpers
context 'when signed in' do
diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
index 1b6215c1308..197a33c355d 100644
--- a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
+++ b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Profile > Notifications > User changes notified_of_own_activity setting', :js do
+RSpec.describe 'Profile > Notifications > User changes notified_of_own_activity setting', :js,
+feature_category: :users do
let(:user) { create(:user) }
before do
diff --git a/spec/features/profiles/user_edit_preferences_spec.rb b/spec/features/profiles/user_edit_preferences_spec.rb
index c724de04043..1a231f1d269 100644
--- a/spec/features/profiles/user_edit_preferences_spec.rb
+++ b/spec/features/profiles/user_edit_preferences_spec.rb
@@ -2,16 +2,18 @@
require 'spec_helper'
-RSpec.describe 'User edit preferences profile', :js do
+RSpec.describe 'User edit preferences profile', :js, feature_category: :users do
include StubLanguagesTranslationPercentage
# Empty value doesn't change the levels
let(:language_percentage_levels) { nil }
let(:user) { create(:user) }
+ let(:vscode_web_ide) { true }
before do
stub_languages_translation_percentage(language_percentage_levels)
stub_feature_flags(user_time_settings: true)
+ stub_feature_flags(vscode_web_ide: vscode_web_ide)
sign_in(user)
visit(profile_preferences_path)
end
@@ -36,6 +38,24 @@ RSpec.describe 'User edit preferences profile', :js do
expect(field).not_to be_checked
end
+ it 'allows the user to toggle using the legacy web ide' do
+ field = page.find_field("user[use_legacy_web_ide]")
+
+ expect(field).not_to be_checked
+
+ field.click
+
+ expect(field).to be_checked
+ end
+
+ describe 'when vscode_web_ide feature flag is disabled' do
+ let(:vscode_web_ide) { false }
+
+ it 'does not display the legacy web ide user preference' do
+ expect(page).not_to have_field("user[use_legacy_web_ide]")
+ end
+ end
+
describe 'User changes tab width to acceptable value' do
it 'shows success message' do
fill_in 'Tab width', with: 9
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index 1d99f7a8511..67604292090 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User edit profile' do
+RSpec.describe 'User edit profile', feature_category: :users do
include Spec::Support::Helpers::Features::NotesHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/profiles/user_manages_applications_spec.rb b/spec/features/profiles/user_manages_applications_spec.rb
index ea7a6b4b6ba..179da61b8ed 100644
--- a/spec/features/profiles/user_manages_applications_spec.rb
+++ b/spec/features/profiles/user_manages_applications_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User manages applications' do
+RSpec.describe 'User manages applications', feature_category: :users do
let_it_be(:user) { create(:user) }
let_it_be(:new_application_path) { applications_profile_path }
let_it_be(:index_path) { oauth_applications_path }
diff --git a/spec/features/profiles/user_manages_emails_spec.rb b/spec/features/profiles/user_manages_emails_spec.rb
index b037d5048aa..16a9fbc2f47 100644
--- a/spec/features/profiles/user_manages_emails_spec.rb
+++ b/spec/features/profiles/user_manages_emails_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User manages emails' do
+RSpec.describe 'User manages emails', feature_category: :users do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
diff --git a/spec/features/profiles/user_search_settings_spec.rb b/spec/features/profiles/user_search_settings_spec.rb
index 0b05e6c9489..09ee8ddeaab 100644
--- a/spec/features/profiles/user_search_settings_spec.rb
+++ b/spec/features/profiles/user_search_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches their settings', :js do
+RSpec.describe 'User searches their settings', :js, feature_category: :users do
let_it_be(:user) { create(:user) }
before do
diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb
index e960cc76219..d212982f4e3 100644
--- a/spec/features/profiles/user_visits_notifications_tab_spec.rb
+++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User visits the notifications tab', :js do
+RSpec.describe 'User visits the notifications tab', :js, feature_category: :users do
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/features/profiles/user_visits_profile_account_page_spec.rb b/spec/features/profiles/user_visits_profile_account_page_spec.rb
index b4d1185412b..1cf34478ecf 100644
--- a/spec/features/profiles/user_visits_profile_account_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_account_page_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User visits the profile account page' do
+RSpec.describe 'User visits the profile account page', feature_category: :users do
let(:user) { create(:user) }
before do
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 22292eff9a3..726cca4a4bd 100644
--- a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
+++ b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User visits the authentication log' do
+RSpec.describe 'User visits the authentication log', feature_category: :users do
let(:user) { create(:user) }
context 'when user signed in' 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 4c61e8d45e4..9eee1b85e5e 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User visits the profile preferences page', :js do
+RSpec.describe 'User visits the profile preferences page', :js, feature_category: :users do
include Select2Helper
let(:user) { create(:user) }
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index df096c2f151..7fca0f24deb 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User visits their profile' do
+RSpec.describe 'User visits their profile', feature_category: :users do
let_it_be_with_refind(:user) { create(:user) }
before do
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
index 0531434f00c..8467e9abeaf 100644
--- a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User visits the profile SSH keys page' do
+RSpec.describe 'User visits the profile SSH keys page', feature_category: :users do
let(:user) { create(:user) }
before do
diff --git a/spec/features/project_group_variables_spec.rb b/spec/features/project_group_variables_spec.rb
index fc482261fb1..0e1e6e49c6d 100644
--- a/spec/features/project_group_variables_spec.rb
+++ b/spec/features/project_group_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project group variables', :js do
+RSpec.describe 'Project group variables', :js, feature_category: :pipeline_authoring do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb
index 33b4af3b5aa..d1258937ce6 100644
--- a/spec/features/project_variables_spec.rb
+++ b/spec/features/project_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project variables', :js do
+RSpec.describe 'Project variables', :js, feature_category: :pipeline_authoring do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:variable) { create(:ci_variable, key: 'test_key', value: 'test_value', masked: true) }
diff --git a/spec/features/projects/active_tabs_spec.rb b/spec/features/projects/active_tabs_spec.rb
index ff97d3c9503..c27c9530f61 100644
--- a/spec/features/projects/active_tabs_spec.rb
+++ b/spec/features/projects/active_tabs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project active tab' do
+RSpec.describe 'Project active tab', feature_category: :projects do
let_it_be(:project) { create(:project, :repository, :with_namespace_settings) }
let(:user) { project.first_owner }
diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb
index a3e511b5c22..5297f30220d 100644
--- a/spec/features/projects/activity/rss_spec.rb
+++ b/spec/features/projects/activity/rss_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Activity RSS' do
+RSpec.describe 'Project Activity RSS', feature_category: :projects do
let(:project) { create(:project, :public) }
let(:user) { project.first_owner }
let(:path) { activity_project_path(project) }
diff --git a/spec/features/projects/activity/user_sees_activity_spec.rb b/spec/features/projects/activity/user_sees_activity_spec.rb
index a9cdbd5c342..cfa62415c49 100644
--- a/spec/features/projects/activity/user_sees_activity_spec.rb
+++ b/spec/features/projects/activity/user_sees_activity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Activity > User sees activity' do
+RSpec.describe 'Projects > Activity > User sees activity', feature_category: :projects do
let(:project) { create(:project, :repository, :public) }
let(:user) { project.creator }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/features/projects/activity/user_sees_design_activity_spec.rb b/spec/features/projects/activity/user_sees_design_activity_spec.rb
index 70153921b82..6a51e548700 100644
--- a/spec/features/projects/activity/user_sees_design_activity_spec.rb
+++ b/spec/features/projects/activity/user_sees_design_activity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Activity > User sees design Activity', :js do
+RSpec.describe 'Projects > Activity > User sees design Activity', :js, feature_category: :design_management do
include DesignManagementTestHelpers
let_it_be(:uploader) { create(:user) }
diff --git a/spec/features/projects/activity/user_sees_design_comment_spec.rb b/spec/features/projects/activity/user_sees_design_comment_spec.rb
index 3a8e2790858..2d333e55b13 100644
--- a/spec/features/projects/activity/user_sees_design_comment_spec.rb
+++ b/spec/features/projects/activity/user_sees_design_comment_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Activity > User sees design comment', :js do
+RSpec.describe 'Projects > Activity > User sees design comment', :js, feature_category: :design_management do
include DesignManagementTestHelpers
let_it_be(:project) { create(:project, :repository, :public) }
diff --git a/spec/features/projects/activity/user_sees_private_activity_spec.rb b/spec/features/projects/activity/user_sees_private_activity_spec.rb
index 86692bc6b4c..e0aaf1dbbc3 100644
--- a/spec/features/projects/activity/user_sees_private_activity_spec.rb
+++ b/spec/features/projects/activity/user_sees_private_activity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project > Activity > User sees private activity', :js do
+RSpec.describe 'Project > Activity > User sees private activity', :js, feature_category: :projects do
let(:project) { create(:project, :public) }
let(:author) { create(:user) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb
index f97c1b0e543..fe38cbc70f1 100644
--- a/spec/features/projects/artifacts/file_spec.rb
+++ b/spec/features/projects/artifacts/file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Artifact file', :js do
+RSpec.describe 'Artifact file', :js, feature_category: :build_artifacts do
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
diff --git a/spec/features/projects/artifacts/raw_spec.rb b/spec/features/projects/artifacts/raw_spec.rb
index c10cb56a44b..544875d36e3 100644
--- a/spec/features/projects/artifacts/raw_spec.rb
+++ b/spec/features/projects/artifacts/raw_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Raw artifact' do
+RSpec.describe 'Raw artifact', feature_category: :build_artifacts do
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
index c0d710fe186..6948a26196b 100644
--- a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
+++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User browses artifacts" do
+RSpec.describe "User browses artifacts", feature_category: :build_artifacts do
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
diff --git a/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb
index 7d6ae03e08e..48dcb95e09b 100644
--- a/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb
+++ b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User downloads artifacts" do
+RSpec.describe "User downloads artifacts", feature_category: :build_artifacts do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:pipeline) { create(:ci_empty_pipeline, status: :success, sha: project.commit.id, project: project) }
let_it_be(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) }
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index 7555e567c37..3c8b17607ca 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'test coverage badge' do
+RSpec.describe 'test coverage badge', feature_category: :code_testing do
let!(:user) { create(:user) }
let!(:project) { create(:project, :private) }
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index d1e635f11c0..e6bd4b22b0a 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'list of badges' do
+RSpec.describe 'list of badges', feature_category: :continuous_integration do
before do
user = create(:user)
project = create(:project, :repository)
diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb
index e3a01ab6fa2..c0f5d0ffead 100644
--- a/spec/features/projects/badges/pipeline_badge_spec.rb
+++ b/spec/features/projects/badges/pipeline_badge_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Pipeline Badge' do
+RSpec.describe 'Pipeline Badge', feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository, :public) }
let(:ref) { project.default_branch }
diff --git a/spec/features/projects/blobs/blame_spec.rb b/spec/features/projects/blobs/blame_spec.rb
index 5287d5e4f7d..27b7c6ef2d5 100644
--- a/spec/features/projects/blobs/blame_spec.rb
+++ b/spec/features/projects/blobs/blame_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'File blame', :js do
+RSpec.describe 'File blame', :js, feature_category: :projects do
include TreeHelper
let_it_be(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
index 9b0edcd09d2..48ee39dad19 100644
--- a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
+++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js do
+RSpec.describe 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js, feature_category: :projects do
include TreeHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index e01382cf31f..b7e0e3fd590 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'File blob', :js do
+RSpec.describe 'File blob', :js, feature_category: :projects do
include MobileHelpers
let(:project) { create(:project, :public, :repository) }
@@ -589,76 +589,35 @@ RSpec.describe 'File blob', :js do
file_path: '.gitlab/dashboards/custom-dashboard.yml',
file_content: file_content
).execute
- end
-
- context 'with metrics_dashboard_exhaustive_validations feature flag off' do
- before do
- stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
- visit_blob('.gitlab/dashboards/custom-dashboard.yml')
- end
-
- context 'valid dashboard file' do
- let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
-
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- # shows that dashboard yaml is valid
- expect(page).to have_content('Metrics Dashboard YAML definition is valid.')
-
- # shows a learn more link
- expect(page).to have_link('Learn more')
- end
- end
- end
-
- context 'invalid dashboard file' do
- let(:file_content) { "dashboard: 'invalid'" }
-
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- # shows that dashboard yaml is invalid
- expect(page).to have_content('Metrics Dashboard YAML definition is invalid:')
- expect(page).to have_content("panel_groups: should be an array of panel_groups objects")
- # shows a learn more link
- expect(page).to have_link('Learn more')
- end
- end
- end
+ visit_blob('.gitlab/dashboards/custom-dashboard.yml')
end
- context 'with metrics_dashboard_exhaustive_validations feature flag on' do
- before do
- stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
- visit_blob('.gitlab/dashboards/custom-dashboard.yml')
- end
-
- context 'valid dashboard file' do
- let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
+ context 'valid dashboard file' do
+ let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) }
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- # shows that dashboard yaml is valid
- expect(page).to have_content('Metrics Dashboard YAML definition is valid.')
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that dashboard yaml is valid
+ expect(page).to have_content('Metrics Dashboard YAML definition is valid.')
- # shows a learn more link
- expect(page).to have_link('Learn more')
- end
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
end
end
+ end
- context 'invalid dashboard file' do
- let(:file_content) { "dashboard: 'invalid'" }
+ context 'invalid dashboard file' do
+ let(:file_content) { "dashboard: 'invalid'" }
- it 'displays an auxiliary viewer' do
- aggregate_failures do
- # shows that dashboard yaml is invalid
- expect(page).to have_content('Metrics Dashboard YAML definition is invalid:')
- expect(page).to have_content("root is missing required keys: panel_groups")
+ it 'displays an auxiliary viewer' do
+ aggregate_failures do
+ # shows that dashboard yaml is invalid
+ expect(page).to have_content('Metrics Dashboard YAML definition is invalid:')
+ expect(page).to have_content("panel_groups: should be an array of panel_groups objects")
- # shows a learn more link
- expect(page).to have_link('Learn more')
- end
+ # shows a learn more link
+ expect(page).to have_link('Learn more')
end
end
end
@@ -1002,7 +961,7 @@ RSpec.describe 'File blob', :js do
end
it 'renders sandboxed iframe' do
- expected = %(<iframe src="/-/sandbox/swagger" sandbox="allow-scripts allow-popups" frameborder="0" width="100%" height="1000">)
+ expected = %(<iframe src="/-/sandbox/swagger" sandbox="allow-scripts allow-popups allow-forms" frameborder="0" width="100%" height="1000">)
expect(page.html).to include(expected)
end
end
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 5587b8abab3..144b4ed85cd 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Editing file blob', :js do
+RSpec.describe 'Editing file blob', :js, feature_category: :projects do
include Spec::Support::Helpers::Features::SourceEditorSpecHelpers
include TreeHelper
include BlobSpecHelpers
@@ -97,11 +97,16 @@ RSpec.describe 'Editing file blob', :js do
"Add a table"
]
- before do
+ it "does not have any buttons" do
+ stub_feature_flags(source_editor_toolbar: true)
visit project_edit_blob_path(project, tree_join(branch, readme_file_path))
+ buttons = page.all('.file-buttons .md-header-toolbar button[type="button"]')
+ expect(buttons.length).to eq(0)
end
- it "has defined set of toolbar buttons" do
+ it "has defined set of toolbar buttons when the flag is off" do
+ stub_feature_flags(source_editor_toolbar: false)
+ visit project_edit_blob_path(project, tree_join(branch, readme_file_path))
buttons = page.all('.file-buttons .md-header-toolbar button[type="button"]')
expect(buttons.length).to eq(toolbar_buttons.length)
toolbar_buttons.each_with_index do |button_title, i|
diff --git a/spec/features/projects/blobs/shortcuts_blob_spec.rb b/spec/features/projects/blobs/shortcuts_blob_spec.rb
index 64d643aa102..03276a737da 100644
--- a/spec/features/projects/blobs/shortcuts_blob_spec.rb
+++ b/spec/features/projects/blobs/shortcuts_blob_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Blob shortcuts', :js do
+RSpec.describe 'Blob shortcuts', :js, feature_category: :projects do
include TreeHelper
let(:project) { create(:project, :public, :repository) }
let(:path) { project.repository.ls_files(project.repository.root_ref)[0] }
diff --git a/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb b/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
index a2db5e11c7c..a497be4cbc3 100644
--- a/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
+++ b/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User follows pipeline suggest nudge spec when feature is enabled', :js do
+RSpec.describe 'User follows pipeline suggest nudge spec when feature is enabled', :js, feature_category: :projects do
include CookieHelper
let(:project) { create(:project, :empty_repo) }
diff --git a/spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb b/spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb
index 15e7a495e60..2f67e909543 100644
--- a/spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb
+++ b/spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views pipeline editor button on root ci config file', :js do
+RSpec.describe 'User views pipeline editor button on root ci config file', :js, feature_category: :projects do
include BlobSpecHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
index 569a93a55fc..80ccd9c1417 100644
--- a/spec/features/projects/branches/download_buttons_spec.rb
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Download buttons in branches page' do
+RSpec.describe 'Download buttons in branches page', feature_category: :projects do
let(:user) { create(:user) }
let(:role) { :developer }
let(:status) { 'success' }
diff --git a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
index 5f58e446ed9..eb370cfc1fc 100644
--- a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
+++ b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
@@ -2,10 +2,11 @@
require 'spec_helper'
-RSpec.describe 'New Branch Ref Dropdown', :js do
+RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
- let(:toggle) { find('.create-from .dropdown-menu-toggle') }
+ let(:sha) { project.commit.sha }
+ let(:toggle) { find('.ref-selector') }
before do
project.add_maintainer(user)
@@ -14,37 +15,75 @@ RSpec.describe 'New Branch Ref Dropdown', :js do
visit new_project_branch_path(project)
end
- it 'filters a list of branches and tags' do
+ it 'finds a tag in a list' do
+ tag_name = 'v1.0.0'
+
toggle.click
- filter_by('v1.0.0')
+ filter_by(tag_name)
+
+ wait_for_requests
+
+ expect(items_count(tag_name)).to be(1)
- expect(items_count).to be(1)
+ item(tag_name).click
- filter_by('video')
+ expect(toggle).to have_content tag_name
+ end
+
+ it 'finds a branch in a list' do
+ branch_name = 'audio'
- expect(items_count).to be(1)
+ toggle.click
- find('.create-from .dropdown-content li').click
+ filter_by(branch_name)
- expect(toggle).to have_content 'video'
+ wait_for_requests
+
+ expect(items_count(branch_name)).to be(1)
+
+ item(branch_name).click
+
+ expect(toggle).to have_content branch_name
end
- it 'accepts a manually entered commit SHA' do
+ it 'finds a commit in a list' do
toggle.click
- filter_by('somecommitsha')
+ filter_by(sha)
+
+ wait_for_requests
+
+ sha_short = sha[0, 7]
- find('.create-from input[type=search]').send_keys(:enter)
+ expect(items_count(sha_short)).to be(1)
+
+ item(sha_short).click
+
+ expect(toggle).to have_content sha_short
+ end
+
+ it 'shows no results when there is no branch, tag or commit sha found' do
+ non_existing_ref = 'non_existing_branch_name'
+
+ toggle.click
+
+ filter_by(non_existing_ref)
+
+ wait_for_requests
+
+ expect(find('.gl-dropdown-contents')).not_to have_content(non_existing_ref)
+ end
- expect(toggle).to have_content 'somecommitsha'
+ def item(ref_name)
+ find('li', text: ref_name, match: :prefer_exact)
end
- def items_count
- all('.create-from .dropdown-content li').length
+ def items_count(ref_name)
+ all('li', text: ref_name, match: :prefer_exact).length
end
def filter_by(filter_text)
- fill_in 'Filter by Git revision', with: filter_text
+ fill_in _('Search by Git revision'), with: filter_text
end
end
diff --git a/spec/features/projects/branches/user_creates_branch_spec.rb b/spec/features/projects/branches/user_creates_branch_spec.rb
index be236b7ca7e..60bd77393e9 100644
--- a/spec/features/projects/branches/user_creates_branch_spec.rb
+++ b/spec/features/projects/branches/user_creates_branch_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User creates branch', :js do
+RSpec.describe 'User creates branch', :js, feature_category: :projects do
include Spec::Support::Helpers::Features::BranchesHelpers
let_it_be(:group) { create(:group, :public) }
@@ -81,11 +81,7 @@ RSpec.describe 'User creates branch', :js do
it 'does not create new branch' do
invalid_branch_name = '1.0 stable'
- fill_in('branch_name', with: invalid_branch_name)
- page.find('body').click # defocus the branch_name input
-
- select_branch('master')
- click_button('Create branch')
+ create_branch(invalid_branch_name)
expect(page).to have_content('Branch name is invalid')
expect(page).to have_content("can't contain spaces")
diff --git a/spec/features/projects/branches/user_deletes_branch_spec.rb b/spec/features/projects/branches/user_deletes_branch_spec.rb
index a89fed3a78a..92b5f176d2d 100644
--- a/spec/features/projects/branches/user_deletes_branch_spec.rb
+++ b/spec/features/projects/branches/user_deletes_branch_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User deletes branch", :js do
+RSpec.describe "User deletes branch", :js, feature_category: :projects do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/branches/user_views_branches_spec.rb b/spec/features/projects/branches/user_views_branches_spec.rb
index 3f0614532f1..f0a1ba84ec6 100644
--- a/spec/features/projects/branches/user_views_branches_spec.rb
+++ b/spec/features/projects/branches/user_views_branches_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User views branches", :js do
+RSpec.describe "User views branches", :js, feature_category: :projects do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index ecf6349e431..fc7833809b3 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Branches' do
+RSpec.describe 'Branches', feature_category: :projects do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let(:repository) { project.repository }
@@ -143,7 +143,7 @@ RSpec.describe 'Branches' do
click_button "Updated date" # Open sorting dropdown
within '[data-testid="branches-dropdown"]' do
- find('p', text: 'Name').click
+ first('span', text: 'Name').click
end
expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :name))
@@ -154,7 +154,7 @@ RSpec.describe 'Branches' do
click_button "Updated date" # Open sorting dropdown
within '[data-testid="branches-dropdown"]' do
- find('p', text: 'Oldest updated').click
+ first('span', text: 'Oldest updated').click
end
expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_asc))
diff --git a/spec/features/projects/ci/editor_spec.rb b/spec/features/projects/ci/editor_spec.rb
index c96d5f5823f..536152626af 100644
--- a/spec/features/projects/ci/editor_spec.rb
+++ b/spec/features/projects/ci/editor_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Pipeline Editor', :js do
+RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_authoring do
include Spec::Support::Helpers::Features::SourceEditorSpecHelpers
let(:project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/projects/ci/lint_spec.rb b/spec/features/projects/ci/lint_spec.rb
index 8d5f62d8a06..4fea07b18bc 100644
--- a/spec/features/projects/ci/lint_spec.rb
+++ b/spec/features/projects/ci/lint_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'CI Lint', :js do
+RSpec.describe 'CI Lint', :js, feature_category: :pipeline_authoring do
include Spec::Support::Helpers::Features::SourceEditorSpecHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/classification_label_on_project_pages_spec.rb b/spec/features/projects/classification_label_on_project_pages_spec.rb
index 9522e5ce2cf..662b2296234 100644
--- a/spec/features/projects/classification_label_on_project_pages_spec.rb
+++ b/spec/features/projects/classification_label_on_project_pages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Classification label on project pages' do
+RSpec.describe 'Classification label on project pages', feature_category: :projects do
let(:project) do
create(:project, external_authorization_classification_label: 'authorized label')
end
diff --git a/spec/features/projects/cluster_agents_spec.rb b/spec/features/projects/cluster_agents_spec.rb
index 8c557a9c37a..43046db2b6c 100644
--- a/spec/features/projects/cluster_agents_spec.rb
+++ b/spec/features/projects/cluster_agents_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'ClusterAgents', :js do
+RSpec.describe 'ClusterAgents', :js, feature_category: :projects do
let_it_be(:token) { create(:cluster_agent_token, description: 'feature test token') }
let(:agent) { token.agent }
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 5c54b7fda7c..114182982e2 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Gcp Cluster', :js do
+RSpec.describe 'Gcp Cluster', :js, feature_category: :kubernetes_management do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index 527d038f975..34fc0a76c7f 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User Cluster', :js do
+RSpec.describe 'User Cluster', :js, feature_category: :kubernetes_management do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 9e1d66bc73e..3fb586bd143 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Clusters', :js do
+RSpec.describe 'Clusters', :js, feature_category: :projects do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index 7b10f72006f..dfd58a99953 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'project commit pipelines', :js do
+RSpec.describe 'project commit pipelines', :js, feature_category: :continuous_integration do
let(:project) { create(:project, :repository) }
before do
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index fce9fa4fb62..dc8b84283a1 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Cherry-pick Commits', :js do
+RSpec.describe 'Cherry-pick Commits', :js, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
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 a470215186b..c53ac27bb5f 100644
--- a/spec/features/projects/commit/comments/user_adds_comment_spec.rb
+++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User adds a comment on a commit", :js do
+RSpec.describe "User adds a comment on a commit", :js, feature_category: :source_code_management do
include Spec::Support::Helpers::Features::NotesHelpers
include RepoHelpers
diff --git a/spec/features/projects/commit/comments/user_deletes_comments_spec.rb b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb
index 9059f9e4857..a1e7ddb4d6e 100644
--- a/spec/features/projects/commit/comments/user_deletes_comments_spec.rb
+++ b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User deletes comments on a commit", :js do
+RSpec.describe "User deletes comments on a commit", :js, feature_category: :source_code_management do
include Spec::Support::Helpers::Features::NotesHelpers
include Spec::Support::Helpers::ModalHelpers
include RepoHelpers
diff --git a/spec/features/projects/commit/comments/user_edits_comments_spec.rb b/spec/features/projects/commit/comments/user_edits_comments_spec.rb
index 8ac15c9cb7f..9019a981a18 100644
--- a/spec/features/projects/commit/comments/user_edits_comments_spec.rb
+++ b/spec/features/projects/commit/comments/user_edits_comments_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User edits a comment on a commit", :js do
+RSpec.describe "User edits a comment on a commit", :js, feature_category: :source_code_management do
include Spec::Support::Helpers::Features::NotesHelpers
include RepoHelpers
diff --git a/spec/features/projects/commit/diff_notes_spec.rb b/spec/features/projects/commit/diff_notes_spec.rb
index 6cebff1cc9a..f29e0803f61 100644
--- a/spec/features/projects/commit/diff_notes_spec.rb
+++ b/spec/features/projects/commit/diff_notes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Commit diff', :js do
+RSpec.describe 'Commit diff', :js, feature_category: :source_code_management do
include RepoHelpers
let(:user) { create(:user) }
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index 417e14e2376..3611efd1477 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Mini Pipeline Graph in Commit View', :js do
+RSpec.describe 'Mini Pipeline Graph in Commit View', :js, feature_category: :source_code_management do
let(:project) { create(:project, :public, :repository) }
context 'when commit has pipelines' do
diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb
index a7f23f093a3..66a407b5ff6 100644
--- a/spec/features/projects/commit/user_comments_on_commit_spec.rb
+++ b/spec/features/projects/commit/user_comments_on_commit_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User comments on commit", :js do
+RSpec.describe "User comments on commit", :js, feature_category: :source_code_management do
include Spec::Support::Helpers::Features::NotesHelpers
include Spec::Support::Helpers::ModalHelpers
include RepoHelpers
diff --git a/spec/features/projects/commit/user_reverts_commit_spec.rb b/spec/features/projects/commit/user_reverts_commit_spec.rb
index 1c6cf5eb258..8c7b8e6ba32 100644
--- a/spec/features/projects/commit/user_reverts_commit_spec.rb
+++ b/spec/features/projects/commit/user_reverts_commit_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User reverts a commit', :js do
+RSpec.describe 'User reverts a commit', :js, feature_category: :source_code_management do
include RepoHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/commit/user_views_user_status_on_commit_spec.rb b/spec/features/projects/commit/user_views_user_status_on_commit_spec.rb
index cc3c70e66ce..5670ed17eba 100644
--- a/spec/features/projects/commit/user_views_user_status_on_commit_spec.rb
+++ b/spec/features/projects/commit/user_views_user_status_on_commit_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project > Commit > View user status' do
+RSpec.describe 'Project > Commit > View user status', feature_category: :source_code_management do
include RepoHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/commits/multi_view_diff_spec.rb b/spec/features/projects/commits/multi_view_diff_spec.rb
index c0e48b7b86c..b178a1c2171 100644
--- a/spec/features/projects/commits/multi_view_diff_spec.rb
+++ b/spec/features/projects/commits/multi_view_diff_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.shared_examples "no multiple viewers" do |commit_ref|
+RSpec.shared_examples "no multiple viewers", feature_category: :source_code_management do |commit_ref|
let(:ref) { commit_ref }
it "does not display multiple diff viewers" do
diff --git a/spec/features/projects/commits/rss_spec.rb b/spec/features/projects/commits/rss_spec.rb
index b521bb865ae..49da0727fbd 100644
--- a/spec/features/projects/commits/rss_spec.rb
+++ b/spec/features/projects/commits/rss_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Commits RSS' do
+RSpec.describe 'Project Commits RSS', feature_category: :source_code_management do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:path) { project_commits_path(project, :master) }
diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb
index 2719316c5dc..791f626b8d9 100644
--- a/spec/features/projects/commits/user_browses_commits_spec.rb
+++ b/spec/features/projects/commits/user_browses_commits_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User browses commits' do
+RSpec.describe 'User browses commits', feature_category: :source_code_management do
include RepoHelpers
let(:user) { create(:user) }
@@ -208,6 +208,10 @@ RSpec.describe 'User browses commits' do
expect(page).not_to have_link 'Create merge request'
end
+ it 'shows ref switcher with correct text', :js do
+ expect(find('.ref-selector')).to have_text('master')
+ end
+
context 'when click the compare tab' do
before do
wait_for_requests
@@ -220,9 +224,18 @@ RSpec.describe 'User browses commits' do
end
end
- context 'feature branch' do
+ context 'feature branch', :js do
let(:visit_commits_page) do
- visit project_commits_path(project, 'feature')
+ visit project_commits_path(project)
+
+ find('.ref-selector').click
+ wait_for_requests
+
+ page.within('.ref-selector') do
+ fill_in 'Search by Git revision', with: 'feature'
+ wait_for_requests
+ find('li', text: 'feature', match: :prefer_exact).click
+ end
end
context 'when project does not have open merge requests' do
@@ -230,6 +243,10 @@ RSpec.describe 'User browses commits' do
visit_commits_page
end
+ it 'shows ref switcher with correct text' do
+ expect(find('.ref-selector')).to have_text('feature')
+ end
+
it 'renders project commits' do
commit = project.repository.commit('0b4bc9a')
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 22b0f344606..8284299443f 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "Compare", :js do
+RSpec.describe "Compare", :js, feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -17,10 +17,10 @@ RSpec.describe "Compare", :js do
visit project_compare_index_path(project, from: 'master', to: 'master')
select_using_dropdown 'from', 'feature'
- expect(find('.js-compare-from-dropdown .gl-new-dropdown-button-text')).to have_content('feature')
+ expect(find('.js-compare-from-dropdown .gl-dropdown-button-text')).to have_content('feature')
select_using_dropdown 'to', 'binary-encoding'
- expect(find('.js-compare-to-dropdown .gl-new-dropdown-button-text')).to have_content('binary-encoding')
+ expect(find('.js-compare-to-dropdown .gl-dropdown-button-text')).to have_content('binary-encoding')
click_button 'Compare'
@@ -32,8 +32,8 @@ RSpec.describe "Compare", :js do
it "pre-populates fields" do
visit project_compare_index_path(project, from: "master", to: "master")
- expect(find(".js-compare-from-dropdown .gl-new-dropdown-button-text")).to have_content("master")
- expect(find(".js-compare-to-dropdown .gl-new-dropdown-button-text")).to have_content("master")
+ expect(find(".js-compare-from-dropdown .gl-dropdown-button-text")).to have_content("master")
+ expect(find(".js-compare-to-dropdown .gl-dropdown-button-text")).to have_content("master")
end
it_behaves_like 'compares branches'
@@ -99,7 +99,7 @@ RSpec.describe "Compare", :js do
find(".js-compare-from-dropdown .compare-dropdown-toggle").click
- expect(find(".js-compare-from-dropdown .gl-new-dropdown-contents")).to have_selector('li.gl-new-dropdown-item', count: 1)
+ expect(find(".js-compare-from-dropdown .gl-dropdown-contents")).to have_selector('li.gl-dropdown-item', count: 1)
end
context 'when commit has overflow', :js do
@@ -153,10 +153,10 @@ RSpec.describe "Compare", :js do
visit project_compare_index_path(project, from: "master", to: "master")
select_using_dropdown "from", "v1.0.0"
- expect(find(".js-compare-from-dropdown .gl-new-dropdown-button-text")).to have_content("v1.0.0")
+ expect(find(".js-compare-from-dropdown .gl-dropdown-button-text")).to have_content("v1.0.0")
select_using_dropdown "to", "v1.1.0"
- expect(find(".js-compare-to-dropdown .gl-new-dropdown-button-text")).to have_content("v1.1.0")
+ expect(find(".js-compare-to-dropdown .gl-dropdown-button-text")).to have_content("v1.1.0")
click_button "Compare"
expect(page).to have_content "Commits"
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 49e7839f16c..c1ce6ea4536 100644
--- a/spec/features/projects/confluence/user_views_confluence_page_spec.rb
+++ b/spec/features/projects/confluence/user_views_confluence_page_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views the Confluence page' do
+RSpec.describe 'User views the Confluence page', feature_category: :integrations do
let_it_be(:user) { create(:user) }
let(:project) { create(:project, :public) }
diff --git a/spec/features/projects/container_registry_spec.rb b/spec/features/projects/container_registry_spec.rb
index e99af734c43..98cf024afa8 100644
--- a/spec/features/projects/container_registry_spec.rb
+++ b/spec/features/projects/container_registry_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Container Registry', :js do
+RSpec.describe 'Container Registry', :js, feature_category: :projects do
include_context 'container registry tags'
let(:user) { create(:user) }
@@ -104,8 +104,6 @@ RSpec.describe 'Container Registry', :js do
find('.modal .modal-footer .btn-danger').click
end
- it_behaves_like 'rejecting tags destruction for an importing repository on', tags: ['1']
-
it('pagination navigate to the second page') do
visit_next_page
diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb
index 06462263f5a..bd48fb68304 100644
--- a/spec/features/projects/deploy_keys_spec.rb
+++ b/spec/features/projects/deploy_keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project deploy keys', :js do
+RSpec.describe 'Project deploy keys', :js, feature_category: :projects do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project_empty_repo) }
let_it_be(:deploy_keys_project) { create(:deploy_keys_project, project: project) }
diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb
index dcd6f1239bb..973c61de31d 100644
--- a/spec/features/projects/diffs/diff_show_spec.rb
+++ b/spec/features/projects/diffs/diff_show_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Diff file viewer', :js, :with_clean_rails_cache do
+RSpec.describe 'Diff file viewer', :js, :with_clean_rails_cache, feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
def visit_commit(sha, anchor: nil)
diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb
index d486d8cf551..4a112445ab9 100644
--- a/spec/features/projects/environments/environment_metrics_spec.rb
+++ b/spec/features/projects/environments/environment_metrics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Environment > Metrics' do
+RSpec.describe 'Environment > Metrics', feature_category: :projects do
include PrometheusHelpers
let(:user) { create(:user) }
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 706c880d097..75913082803 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Environment' do
+RSpec.describe 'Environment', feature_category: :projects do
let_it_be(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:role) { :developer }
@@ -10,12 +10,83 @@ RSpec.describe 'Environment' do
before do
sign_in(user)
project.add_role(user, role)
+ stub_feature_flags(environment_details_vue: false)
end
def auto_stop_button_selector
%q{button[title="Prevent environment from auto-stopping"]}
end
+ describe 'environment details page vue' do
+ let_it_be(:environment) { create(:environment, project: project) }
+ let!(:permissions) {}
+ let!(:deployment) {}
+ let!(:action) {}
+ let!(:cluster) {}
+
+ before do
+ stub_feature_flags(environment_details_vue: true)
+ end
+
+ context 'with auto-stop' do
+ let_it_be(:environment) { create(:environment, :will_auto_stop, name: 'staging', project: project) }
+
+ before do
+ visit_environment(environment)
+ end
+
+ it 'shows auto stop info', :js do
+ expect(page).to have_content('Auto stops')
+ end
+
+ it 'shows auto stop button', :js do
+ expect(page).to have_selector(auto_stop_button_selector)
+ expect(page.find(auto_stop_button_selector).find(:xpath, '..')['action']).to have_content(cancel_auto_stop_project_environment_path(environment.project, environment))
+ end
+
+ it 'allows user to cancel auto stop', :js do
+ page.find(auto_stop_button_selector).click
+ wait_for_all_requests
+ expect(page).to have_content('Auto stop successfully canceled.')
+ expect(page).not_to have_selector(auto_stop_button_selector)
+ end
+ end
+
+ context 'with deployments' do
+ before do
+ visit_environment(environment)
+ end
+
+ context 'when there is a successful deployment' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+
+ let(:deployment) do
+ create(:deployment, :success, environment: environment, deployable: build)
+ end
+
+ it 'does show deployments', :js do
+ wait_for_requests
+ expect(page).to have_link("#{build.name} (##{build.id})")
+ end
+ end
+
+ context 'when there is a failed deployment' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ let(:deployment) do
+ create(:deployment, :failed, environment: environment, deployable: build)
+ end
+
+ it 'does show deployments', :js do
+ wait_for_requests
+ expect(page).to have_link("#{build.name} (##{build.id})")
+ end
+ end
+ end
+ end
+
describe 'environment details page' do
let_it_be(:environment) { create(:environment, project: project) }
let!(:permissions) {}
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index b445b0da901..788bf6477b1 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Environments page', :js do
+RSpec.describe 'Environments page', :js, feature_category: :projects do
include Spec::Support::Helpers::ModalHelpers
let(:project) { create(:project) }
diff --git a/spec/features/projects/feature_flag_user_lists/user_deletes_feature_flag_user_list_spec.rb b/spec/features/projects/feature_flag_user_lists/user_deletes_feature_flag_user_list_spec.rb
index 37d6f299883..6383c3196c4 100644
--- a/spec/features/projects/feature_flag_user_lists/user_deletes_feature_flag_user_list_spec.rb
+++ b/spec/features/projects/feature_flag_user_lists/user_deletes_feature_flag_user_list_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User deletes feature flag user list', :js do
+RSpec.describe 'User deletes feature flag user list', :js, feature_category: :projects do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
diff --git a/spec/features/projects/feature_flag_user_lists/user_edits_feature_flag_user_list_spec.rb b/spec/features/projects/feature_flag_user_lists/user_edits_feature_flag_user_list_spec.rb
index b37c2780827..8ab9e9baab9 100644
--- a/spec/features/projects/feature_flag_user_lists/user_edits_feature_flag_user_list_spec.rb
+++ b/spec/features/projects/feature_flag_user_lists/user_edits_feature_flag_user_list_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User edits feature flag user list', :js do
+RSpec.describe 'User edits feature flag user list', :js, feature_category: :projects do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
diff --git a/spec/features/projects/feature_flag_user_lists/user_sees_feature_flag_user_list_details_spec.rb b/spec/features/projects/feature_flag_user_lists/user_sees_feature_flag_user_list_details_spec.rb
index dfebe6408bd..7614349c5a4 100644
--- a/spec/features/projects/feature_flag_user_lists/user_sees_feature_flag_user_list_details_spec.rb
+++ b/spec/features/projects/feature_flag_user_lists/user_sees_feature_flag_user_list_details_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User sees feature flag user list details', :js do
+RSpec.describe 'User sees feature flag user list details', :js, feature_category: :projects do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
diff --git a/spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb b/spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb
index 43540dc4522..852d7bca96a 100644
--- a/spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb
+++ b/spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User deletes feature flag', :js do
+RSpec.describe 'User deletes feature flag', :js, feature_category: :feature_flags do
include FeatureFlagHelpers
let(:user) { create(:user) }
diff --git a/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb b/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb
index 949e530f86d..e2448887531 100644
--- a/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb
+++ b/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User sees feature flag list', :js do
+RSpec.describe 'User sees feature flag list', :js, feature_category: :feature_flags do
include FeatureFlagHelpers
let_it_be(:user) { create(:user) }
@@ -42,7 +42,7 @@ RSpec.describe 'User sees feature flag list', :js do
expect_status_toggle_button_not_to_be_checked
within_feature_flag_scopes do
- expect(page.find('[data-testid="strategy-badge"]')).to have_content('All Users: All Environments, review/*')
+ expect(page.find('[data-testid="strategy-label"]')).to have_content('All Users: All Environments, review/*')
end
end
end
@@ -66,7 +66,7 @@ RSpec.describe 'User sees feature flag list', :js do
expect_status_toggle_button_to_be_checked
within_feature_flag_scopes do
- expect(page.find('[data-testid="strategy-badge"]')).to have_content('All Users: production')
+ expect(page.find('[data-testid="strategy-label"]')).to have_content('All Users: production')
end
end
end
diff --git a/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb b/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb
index eb9ac078662..ce99ae92d63 100644
--- a/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb
+++ b/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User updates feature flag', :js do
+RSpec.describe 'User updates feature flag', :js, feature_category: :feature_flags do
include FeatureFlagHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 649c21d4459..5e0998412ed 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Edit Project Settings' do
+RSpec.describe 'Edit Project Settings', feature_category: :projects do
let(:member) { create(:user) }
let!(:project) { create(:project, :public, :repository) }
let!(:issue) { create(:issue, project: project) }
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
index dd1635c900e..1e05bdae204 100644
--- a/spec/features/projects/files/dockerfile_dropdown_spec.rb
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', :js do
+RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', :js, feature_category: :projects do
include Spec::Support::Helpers::Features::SourceEditorSpecHelpers
before do
diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb
index a486d7517ac..2710e2efa94 100644
--- a/spec/features/projects/files/download_buttons_spec.rb
+++ b/spec/features/projects/files/download_buttons_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > Download buttons in files tree' do
+RSpec.describe 'Projects > Files > Download buttons in files tree', feature_category: :projects do
let(:project) { create(:project, :repository) }
let(:user) { project.creator }
diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
index e08c53a67dd..f6342257847 100644
--- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb
+++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js do
+RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js, feature_category: :projects do
before do
project = create(:project, :repository)
user = project.first_owner
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
index e256bec2a1c..04f45de42cc 100644
--- a/spec/features/projects/files/editing_a_file_spec.rb
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User wants to edit a file' do
+RSpec.describe 'Projects > Files > User wants to edit a file', feature_category: :projects do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
let(:commit_params) do
diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
index a283f7d128c..d791e22e4f8 100644
--- a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
+++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User views files page' do
+RSpec.describe 'Projects > Files > User views files page', feature_category: :projects do
let(:project) { create(:forked_project_with_submodules) }
let(:user) { project.first_owner }
diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
index 9ae3be4993b..19813396435 100644
--- a/spec/features/projects/files/find_file_keyboard_spec.rb
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > Find file keyboard shortcuts', :js do
+RSpec.describe 'Projects > Files > Find file keyboard shortcuts', :js, feature_category: :projects do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index a86adf951d8..5e11a94e65b 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User wants to add a .gitignore file', :js do
+RSpec.describe 'Projects > Files > User wants to add a .gitignore file', :js, feature_category: :projects do
include Spec::Support::Helpers::Features::SourceEditorSpecHelpers
before do
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
index 46ac0dee7eb..67678a937e5 100644
--- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file', :js do
+RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file', :js, feature_category: :projects do
include Spec::Support::Helpers::Features::SourceEditorSpecHelpers
let(:params) { {} }
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 c9ba8cbd2bb..7ac9cb33060 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > Project owner creates a license file', :js do
+RSpec.describe 'Projects > Files > Project owner creates a license file', :js, feature_category: :projects do
let(:project) { create(:project, :repository) }
let(:project_maintainer) { project.first_owner }
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 52686469243..8d64151e680 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > Project owner sees a link to create a license file in empty project', :js do
+RSpec.describe 'Projects > Files > Project owner sees a link to create a license file in empty project', :js,
+feature_category: :projects do
include WebIdeSpecHelpers
let(:project) { create(:project_empty_repo) }
diff --git a/spec/features/projects/files/template_selector_menu_spec.rb b/spec/features/projects/files/template_selector_menu_spec.rb
index 51ae6616d4a..8dbfa3afb0b 100644
--- a/spec/features/projects/files/template_selector_menu_spec.rb
+++ b/spec/features/projects/files/template_selector_menu_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Template selector menu', :js do
+RSpec.describe 'Template selector menu', :js, feature_category: :projects do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb
index 9cdb5eeb076..990b118d172 100644
--- a/spec/features/projects/files/template_type_dropdown_spec.rb
+++ b/spec/features/projects/files/template_type_dropdown_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > Template type dropdown selector', :js do
+RSpec.describe 'Projects > Files > Template type dropdown selector', :js, feature_category: :projects do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb
index 0b2daf12063..afc9a5fd232 100644
--- a/spec/features/projects/files/undo_template_spec.rb
+++ b/spec/features/projects/files/undo_template_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > Template Undo Button', :js do
+RSpec.describe 'Projects > Files > Template Undo Button', :js, feature_category: :projects do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
diff --git a/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb b/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb
index 220572c6a6d..8b60d21a77e 100644
--- a/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb
+++ b/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
# This is a regression test for https://gitlab.com/gitlab-org/gitlab-foss/issues/37569
-RSpec.describe 'Projects > Files > User browses a tree with a folder containing only a folder', :js do
+RSpec.describe 'Projects > Files > User browses a tree with a folder containing only a folder', :js,
+feature_category: :projects do
let(:project) { create(:project, :empty_repo) }
let(:user) { project.first_owner }
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 0f3ce5a2bad..125f7209ab4 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User browses files", :js do
+RSpec.describe "User browses files", :js, feature_category: :projects do
include RepoHelpers
let(:fork_message) do
@@ -86,6 +86,15 @@ RSpec.describe "User browses files", :js do
visit(project_tree_path(project, "markdown"))
end
+ it "redirects to the permalink URL" do
+ click_link(".gitignore")
+ click_link("Permalink")
+
+ permalink_path = project_blob_path(project, "#{project.repository.commit('markdown').sha}/.gitignore")
+
+ expect(page).to have_current_path(permalink_path, ignore_query: true)
+ end
+
it "shows correct files and links" do
expect(page).to have_current_path(project_tree_path(project, "markdown"), ignore_query: true)
expect(page).to have_content("README.md")
@@ -262,6 +271,8 @@ RSpec.describe "User browses files", :js do
context "when browsing a specific ref", :js do
let(:ref) { project_tree_path(project, "6d39438") }
+ ref_selector = '.ref-selector'
+
before do
visit(ref)
end
@@ -272,24 +283,34 @@ RSpec.describe "User browses files", :js do
end
it "shows files from a repository with apostroph in its name" do
- first(".js-project-refs-dropdown").click
+ ref_name = 'test'
- page.within(".project-refs-form") do
- click_link("'test'")
+ find(ref_selector).click
+ wait_for_requests
+
+ page.within(ref_selector) do
+ fill_in 'Search by Git revision', with: ref_name
+ wait_for_requests
+ find('li', text: ref_name, match: :prefer_exact).click
end
- expect(page).to have_selector(".dropdown-toggle-text", text: "'test'")
+ expect(find(ref_selector)).to have_text(ref_name)
- visit(project_tree_path(project, "'test'"))
+ visit(project_tree_path(project, ref_name))
expect(page).not_to have_selector(".tree-commit .animation-container")
end
it "shows the code with a leading dot in the directory" do
- first(".js-project-refs-dropdown").click
+ ref_name = 'fix'
+
+ find(ref_selector).click
+ wait_for_requests
- page.within(".project-refs-form") do
- click_link("fix")
+ page.within(ref_selector) do
+ fill_in 'Search by Git revision', with: ref_name
+ wait_for_requests
+ find('li', text: ref_name, match: :prefer_exact).click
end
visit(project_tree_path(project, "fix/.testdir"))
diff --git a/spec/features/projects/files/user_browses_lfs_files_spec.rb b/spec/features/projects/files/user_browses_lfs_files_spec.rb
index 56e18871810..6b401d6d789 100644
--- a/spec/features/projects/files/user_browses_lfs_files_spec.rb
+++ b/spec/features/projects/files/user_browses_lfs_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User browses LFS files' do
+RSpec.describe 'Projects > Files > User browses LFS files', feature_category: :projects do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb
index 9e0168d7ef3..4dd579ba8e9 100644
--- a/spec/features/projects/files/user_creates_directory_spec.rb
+++ b/spec/features/projects/files/user_creates_directory_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User creates a directory', :js do
+RSpec.describe 'Projects > Files > User creates a directory', :js, feature_category: :projects do
let(:fork_message) do
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb
index a81f31d663e..97ccb45dfc6 100644
--- a/spec/features/projects/files/user_creates_files_spec.rb
+++ b/spec/features/projects/files/user_creates_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User creates files', :js do
+RSpec.describe 'Projects > Files > User creates files', :js, feature_category: :projects do
include Spec::Support::Helpers::Features::SourceEditorSpecHelpers
include BlobSpecHelpers
diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb
index 806f1e8e9ed..61152a8badc 100644
--- a/spec/features/projects/files/user_deletes_files_spec.rb
+++ b/spec/features/projects/files/user_deletes_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User deletes files', :js do
+RSpec.describe 'Projects > Files > User deletes files', :js, feature_category: :projects do
let(:fork_message) do
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index 1a9c5483218..5a61aa146a2 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User edits files', :js do
+RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :projects do
include Spec::Support::Helpers::Features::SourceEditorSpecHelpers
include ProjectForksHelper
include BlobSpecHelpers
diff --git a/spec/features/projects/files/user_find_file_spec.rb b/spec/features/projects/files/user_find_file_spec.rb
index 69ea8b0eb5f..1b53189da83 100644
--- a/spec/features/projects/files/user_find_file_spec.rb
+++ b/spec/features/projects/files/user_find_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User find project file' do
+RSpec.describe 'User find project file', feature_category: :projects do
let(:user) { create :user }
let(:project) { create :project, :repository }
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 294a03813cd..18a5fb71b10 100644
--- a/spec/features/projects/files/user_reads_pipeline_status_spec.rb
+++ b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'user reads pipeline status', :js do
+RSpec.describe 'user reads pipeline status', :js, feature_category: :projects do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:v110_pipeline) { create_pipeline('v1.1.0', 'success') }
diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb
index 1ecd50b6463..9fa3ddf92c6 100644
--- a/spec/features/projects/files/user_replaces_files_spec.rb
+++ b/spec/features/projects/files/user_replaces_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User replaces files', :js do
+RSpec.describe 'Projects > Files > User replaces files', :js, feature_category: :projects do
include DropzoneHelper
let(:fork_message) do
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 cce73d06f94..b438b203141 100644
--- a/spec/features/projects/files/user_searches_for_files_spec.rb
+++ b/spec/features/projects/files/user_searches_for_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User searches for files' do
+RSpec.describe 'Projects > Files > User searches for files', feature_category: :projects do
let(:user) { project.first_owner }
before do
diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb
index cc621dfd9f8..575a6290a32 100644
--- a/spec/features/projects/files/user_uploads_files_spec.rb
+++ b/spec/features/projects/files/user_uploads_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User uploads files' do
+RSpec.describe 'Projects > Files > User uploads files', feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, name: 'Shop', creator: user) }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index 9ceadb63178..3867f7fd086 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project fork' do
+RSpec.describe 'Project fork', feature_category: :projects do
include ProjectForksHelper
let(:user) { create(:user) }
diff --git a/spec/features/projects/forks/fork_list_spec.rb b/spec/features/projects/forks/fork_list_spec.rb
index b48c46ef8cb..18424c18cbc 100644
--- a/spec/features/projects/forks/fork_list_spec.rb
+++ b/spec/features/projects/forks/fork_list_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'listing forks of a project' do
+RSpec.describe 'listing forks of a project', feature_category: :projects do
include ProjectForksHelper
include ExternalAuthorizationServiceHelpers
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
index a7d68b07dd3..bb9f4e121d8 100644
--- a/spec/features/projects/gfm_autocomplete_load_spec.rb
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'GFM autocomplete loading', :js do
+RSpec.describe 'GFM autocomplete loading', :js, feature_category: :projects do
let(:project) { create(:project) }
before do
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index 0b628ad1e9a..f96356b11c9 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Graph', :js do
+RSpec.describe 'Project Graph', :js, feature_category: :projects do
let(:user) { create :user }
let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:branch_name) { 'master' }
diff --git a/spec/features/projects/hook_logs/user_reads_log_spec.rb b/spec/features/projects/hook_logs/user_reads_log_spec.rb
index 9b7ec14c36f..92ddc559cf4 100644
--- a/spec/features/projects/hook_logs/user_reads_log_spec.rb
+++ b/spec/features/projects/hook_logs/user_reads_log_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Hook logs' do
+RSpec.describe 'Hook logs', feature_category: :projects do
let(:project) { create(:project) }
let(:project_hook) { create(:project_hook, project: project) }
let(:web_hook_log) { create(:web_hook_log, web_hook: project_hook, response_body: 'Hello World') }
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index ccf3ccc6a96..8986ce91ae3 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -6,7 +6,7 @@ require 'spec_helper'
# It looks up for any sensitive word inside the JSON, so if a sensitive word is found
# we'll have to either include it adding the model that includes it to the +safe_list+
# or make sure the attribute is blacklisted in the +import_export.yml+ configuration
-RSpec.describe 'Import/Export - project export integration test', :js do
+RSpec.describe 'Import/Export - project export integration test', :js, feature_category: :importers do
include Select2Helper
include ExportFileHelper
@@ -53,7 +53,7 @@ RSpec.describe 'Import/Export - project export integration test', :js do
project_json_path = File.join(tmpdir, 'project.json')
expect(File).to exist(project_json_path)
- project_hash = Gitlab::Json.parse(IO.read(project_json_path))
+ project_hash = Gitlab::Json.parse(File.read(project_json_path))
sensitive_words.each do |sensitive_word|
found = find_sensitive_attributes(sensitive_word, project_hash)
@@ -79,7 +79,7 @@ RSpec.describe 'Import/Export - project export integration test', :js do
expect(File).to exist(project_json_path)
relations = []
- relations << Gitlab::Json.parse(IO.read(project_json_path))
+ relations << Gitlab::Json.parse(File.read(project_json_path))
Dir.glob(File.join(tmpdir, 'tree/project', '*.ndjson')) do |rb_filename|
File.foreach(rb_filename) do |line|
relations << Gitlab::Json.parse(line)
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 6f015f9cd22..8fb11f06cdd 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Import/Export - project import integration test', :js do
+RSpec.describe 'Import/Export - project import integration test', :js, feature_category: :importers do
let(:user) { create(:user) }
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
diff --git a/spec/features/projects/infrastructure_registry_spec.rb b/spec/features/projects/infrastructure_registry_spec.rb
index aab1cec8762..e1619726c8d 100644
--- a/spec/features/projects/infrastructure_registry_spec.rb
+++ b/spec/features/projects/infrastructure_registry_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Infrastructure Registry' do
+RSpec.describe 'Infrastructure Registry', feature_category: :projects do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/features/projects/integrations/disable_triggers_spec.rb b/spec/features/projects/integrations/disable_triggers_spec.rb
index b039d610ecb..f7afce6d87c 100644
--- a/spec/features/projects/integrations/disable_triggers_spec.rb
+++ b/spec/features/projects/integrations/disable_triggers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Disable individual triggers', :js do
+RSpec.describe 'Disable individual triggers', :js, feature_category: :integrations do
include_context 'project integration activation'
let(:checkbox_selector) { 'input[name$="_events]"]' }
diff --git a/spec/features/projects/integrations/project_integrations_spec.rb b/spec/features/projects/integrations/project_integrations_spec.rb
index 708a5bca8c1..d99b6ca9092 100644
--- a/spec/features/projects/integrations/project_integrations_spec.rb
+++ b/spec/features/projects/integrations/project_integrations_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project integrations', :js do
+RSpec.describe 'Project integrations', :js, feature_category: :integrations do
include_context 'project integration activation'
it_behaves_like 'integration settings form' do
diff --git a/spec/features/projects/integrations/user_activates_asana_spec.rb b/spec/features/projects/integrations/user_activates_asana_spec.rb
index 9ec9f00529a..b99ca2ebc1c 100644
--- a/spec/features/projects/integrations/user_activates_asana_spec.rb
+++ b/spec/features/projects/integrations/user_activates_asana_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates Asana' do
+RSpec.describe 'User activates Asana', feature_category: :integrations do
include_context 'project integration activation'
it 'activates integration', :js do
diff --git a/spec/features/projects/integrations/user_activates_assembla_spec.rb b/spec/features/projects/integrations/user_activates_assembla_spec.rb
index be9034ec5ba..db5774e4514 100644
--- a/spec/features/projects/integrations/user_activates_assembla_spec.rb
+++ b/spec/features/projects/integrations/user_activates_assembla_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates Assembla' do
+RSpec.describe 'User activates Assembla', feature_category: :integrations do
include_context 'project integration activation'
before do
diff --git a/spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb b/spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb
index 49f62a34bd2..a532c1b8644 100644
--- a/spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb
+++ b/spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates Atlassian Bamboo CI' do
+RSpec.describe 'User activates Atlassian Bamboo CI', feature_category: :integrations do
include_context 'project integration activation'
before do
diff --git a/spec/features/projects/integrations/user_activates_emails_on_push_spec.rb b/spec/features/projects/integrations/user_activates_emails_on_push_spec.rb
index 168779aad07..9a2d693a9f0 100644
--- a/spec/features/projects/integrations/user_activates_emails_on_push_spec.rb
+++ b/spec/features/projects/integrations/user_activates_emails_on_push_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates Emails on push' do
+RSpec.describe 'User activates Emails on push', feature_category: :integrations do
include_context 'project integration activation'
it 'activates integration', :js do
diff --git a/spec/features/projects/integrations/user_activates_flowdock_spec.rb b/spec/features/projects/integrations/user_activates_flowdock_spec.rb
deleted file mode 100644
index df1a4feddfb..00000000000
--- a/spec/features/projects/integrations/user_activates_flowdock_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'User activates Flowdock' do
- include_context 'project integration activation' do
- let(:project) { create(:project, :repository) }
- end
-
- before do
- stub_request(:post, /.*api.flowdock.com.*/)
- end
-
- it 'activates integration', :js do
- visit_project_integration('Flowdock')
- fill_in('Token', with: 'verySecret')
-
- click_test_then_save_integration(expect_test_to_fail: false)
-
- expect(page).to have_content('Flowdock settings saved and active.')
- end
-end
diff --git a/spec/features/projects/integrations/user_activates_irker_spec.rb b/spec/features/projects/integrations/user_activates_irker_spec.rb
index 23b5f2a5c47..17c46bfaff7 100644
--- a/spec/features/projects/integrations/user_activates_irker_spec.rb
+++ b/spec/features/projects/integrations/user_activates_irker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates irker (IRC gateway)' do
+RSpec.describe 'User activates irker (IRC gateway)', feature_category: :integrations do
include_context 'project integration activation'
it 'activates integration', :js do
diff --git a/spec/features/projects/integrations/user_activates_jetbrains_teamcity_ci_spec.rb b/spec/features/projects/integrations/user_activates_jetbrains_teamcity_ci_spec.rb
index f86a1b8a0a4..a18c052beb9 100644
--- a/spec/features/projects/integrations/user_activates_jetbrains_teamcity_ci_spec.rb
+++ b/spec/features/projects/integrations/user_activates_jetbrains_teamcity_ci_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates JetBrains TeamCity CI' do
+RSpec.describe 'User activates JetBrains TeamCity CI', feature_category: :integrations do
include_context 'project integration activation'
before do
diff --git a/spec/features/projects/integrations/user_activates_jira_spec.rb b/spec/features/projects/integrations/user_activates_jira_spec.rb
index dad201ffbb6..e4b10aeb340 100644
--- a/spec/features/projects/integrations/user_activates_jira_spec.rb
+++ b/spec/features/projects/integrations/user_activates_jira_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates Jira', :js do
+RSpec.describe 'User activates Jira', :js, feature_category: :integrations do
include_context 'project integration activation'
include_context 'project integration Jira context'
diff --git a/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb
index 54c9ec0f62e..16c7a3ff226 100644
--- a/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Set up Mattermost slash commands', :js do
+RSpec.describe 'Set up Mattermost slash commands', :js, feature_category: :integrations do
describe 'user visits the mattermost slash command config page' do
include_context 'project integration activation'
diff --git a/spec/features/projects/integrations/user_activates_packagist_spec.rb b/spec/features/projects/integrations/user_activates_packagist_spec.rb
index 0892843e840..2d77abfea7c 100644
--- a/spec/features/projects/integrations/user_activates_packagist_spec.rb
+++ b/spec/features/projects/integrations/user_activates_packagist_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates Packagist' do
+RSpec.describe 'User activates Packagist', feature_category: :integrations do
include_context 'project integration activation'
before do
diff --git a/spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb b/spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb
index fe6ed786ace..b4dec8ffdb5 100644
--- a/spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb
+++ b/spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates PivotalTracker' do
+RSpec.describe 'User activates PivotalTracker', feature_category: :integrations do
include_context 'project integration activation'
before do
diff --git a/spec/features/projects/integrations/user_activates_prometheus_spec.rb b/spec/features/projects/integrations/user_activates_prometheus_spec.rb
index 56b895919b8..5b2d885410f 100644
--- a/spec/features/projects/integrations/user_activates_prometheus_spec.rb
+++ b/spec/features/projects/integrations/user_activates_prometheus_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates Prometheus' do
+RSpec.describe 'User activates Prometheus', feature_category: :integrations do
include_context 'project integration activation'
before do
diff --git a/spec/features/projects/integrations/user_activates_pushover_spec.rb b/spec/features/projects/integrations/user_activates_pushover_spec.rb
index 616efdc836f..a705c354a1e 100644
--- a/spec/features/projects/integrations/user_activates_pushover_spec.rb
+++ b/spec/features/projects/integrations/user_activates_pushover_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates Pushover' do
+RSpec.describe 'User activates Pushover', feature_category: :integrations do
include_context 'project integration activation'
before do
diff --git a/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb b/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb
index e89f6e309ea..01c202baf70 100644
--- a/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb
+++ b/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates Slack notifications', :js do
+RSpec.describe 'User activates Slack notifications', :js, feature_category: :integrations do
include_context 'project integration activation'
context 'when integration is not configured yet' do
diff --git a/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb b/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb
index df8cd84ffdb..0f6d721565e 100644
--- a/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb
+++ b/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Slack slash commands', :js do
+RSpec.describe 'Slack slash commands', :js, feature_category: :integrations do
include_context 'project integration activation'
before do
diff --git a/spec/features/projects/integrations/user_uses_inherited_settings_spec.rb b/spec/features/projects/integrations/user_uses_inherited_settings_spec.rb
index 8a2881c95dc..e0063a9c733 100644
--- a/spec/features/projects/integrations/user_uses_inherited_settings_spec.rb
+++ b/spec/features/projects/integrations/user_uses_inherited_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User uses inherited settings', :js do
+RSpec.describe 'User uses inherited settings', :js, feature_category: :integrations do
include JiraIntegrationHelpers
include_context 'project integration activation'
diff --git a/spec/features/projects/integrations/user_views_services_spec.rb b/spec/features/projects/integrations/user_views_services_spec.rb
index 559461f911f..e6be300c0a9 100644
--- a/spec/features/projects/integrations/user_views_services_spec.rb
+++ b/spec/features/projects/integrations/user_views_services_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views integrations', :js do
+RSpec.describe 'User views integrations', :js, feature_category: :integrations do
include_context 'project integration activation'
it 'shows the list of available integrations' do
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index ac83de3e765..adf410ce6e8 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'issuable templates', :js do
+RSpec.describe 'issuable templates', :js, feature_category: :projects do
include ProjectForksHelper
let(:user) { create(:user) }
diff --git a/spec/features/projects/issues/design_management/user_links_to_designs_in_issue_spec.rb b/spec/features/projects/issues/design_management/user_links_to_designs_in_issue_spec.rb
index 78fb470d4ea..ef7022dcda8 100644
--- a/spec/features/projects/issues/design_management/user_links_to_designs_in_issue_spec.rb
+++ b/spec/features/projects/issues/design_management/user_links_to_designs_in_issue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'viewing issues with design references' do
+RSpec.describe 'viewing issues with design references', feature_category: :design_management do
include DesignManagementTestHelpers
let_it_be(:public_project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb b/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb
index 908e30478b2..1490702a964 100644
--- a/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb
+++ b/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User paginates issue designs', :js do
+RSpec.describe 'User paginates issue designs', :js, feature_category: :design_management do
include DesignManagementTestHelpers
let(:project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb b/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb
index cfd8a4540ee..094bc9218ed 100644
--- a/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb
+++ b/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User design permissions', :js do
+RSpec.describe 'User design permissions', :js, feature_category: :design_management do
include DesignManagementTestHelpers
let(:project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
index 27d0be23aec..858d6751afa 100644
--- a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
+++ b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User uploads new design', :js do
+RSpec.describe 'User uploads new design', :js, feature_category: :design_management do
include DesignManagementTestHelpers
let(:project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/projects/issues/design_management/user_views_design_images_spec.rb b/spec/features/projects/issues/design_management/user_views_design_images_spec.rb
index c3aefe05f75..c5fc11222c2 100644
--- a/spec/features/projects/issues/design_management/user_views_design_images_spec.rb
+++ b/spec/features/projects/issues/design_management/user_views_design_images_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Users views raw design image files' do
+RSpec.describe 'Users views raw design image files', feature_category: :design_management do
include DesignManagementTestHelpers
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/features/projects/issues/design_management/user_views_design_spec.rb b/spec/features/projects/issues/design_management/user_views_design_spec.rb
index b513a4fe3fa..11c8bdda3ac 100644
--- a/spec/features/projects/issues/design_management/user_views_design_spec.rb
+++ b/spec/features/projects/issues/design_management/user_views_design_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views issue designs', :js do
+RSpec.describe 'User views issue designs', :js, feature_category: :design_management do
include DesignManagementTestHelpers
let_it_be(:project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/projects/issues/design_management/user_views_designs_spec.rb b/spec/features/projects/issues/design_management/user_views_designs_spec.rb
index 46c772027ad..995ed66df98 100644
--- a/spec/features/projects/issues/design_management/user_views_designs_spec.rb
+++ b/spec/features/projects/issues/design_management/user_views_designs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views issue designs', :js do
+RSpec.describe 'User views issue designs', :js, feature_category: :design_management do
include DesignManagementTestHelpers
let_it_be(:project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb b/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb
index 682a45cf592..a45b9b718c3 100644
--- a/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb
+++ b/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views an SVG design that contains XSS', :js do
+RSpec.describe 'User views an SVG design that contains XSS', :js, feature_category: :design_management do
include DesignManagementTestHelpers
let(:project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/projects/issues/email_participants_spec.rb b/spec/features/projects/issues/email_participants_spec.rb
index 3ffe0a5ced8..4dedbff608e 100644
--- a/spec/features/projects/issues/email_participants_spec.rb
+++ b/spec/features/projects/issues/email_participants_spec.rb
@@ -2,16 +2,15 @@
require 'spec_helper'
-RSpec.describe 'viewing an issue', :js do
+RSpec.describe 'viewing an issue', :js, feature_category: :issue_email_participants do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
- let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be_with_refind(:issue) { create(:issue, project: project) }
let_it_be(:note) { create(:note_on_issue, project: project, noteable: issue) }
let_it_be(:participants) { create_list(:issue_email_participant, 4, issue: issue) }
before do
- sign_in(user)
- visit project_issue_path(project, issue)
+ project.add_reporter(user)
end
shared_examples 'email participants warning' do |selector|
@@ -20,15 +19,48 @@ RSpec.describe 'viewing an issue', :js do
end
end
- context 'for a new note' do
- it_behaves_like 'email participants warning', '.new-note'
+ shared_examples 'no email participants warning' do |selector|
+ it 'does not show email participants warning' do
+ expect(find(selector)).not_to have_content(", and 1 more will be notified of your comment")
+ end
+ end
+
+ context 'when issue is confidential' do
+ before do
+ issue.update!(confidential: true)
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ end
+
+ context 'for a new note' do
+ it_behaves_like 'email participants warning', '.new-note'
+ end
+
+ context 'for a reply form' do
+ before do
+ find('.js-reply-button').click
+ end
+
+ it_behaves_like 'email participants warning', '.note-edit-form'
+ end
end
- context 'for a reply form' do
+ context 'when issue is not confidential' do
before do
- find('.js-reply-button').click
+ sign_in(user)
+ visit project_issue_path(project, issue)
end
- it_behaves_like 'email participants warning', '.note-edit-form'
+ context 'for a new note' do
+ it_behaves_like 'no email participants warning', '.new-note'
+ end
+
+ context 'for a reply form' do
+ before do
+ find('.js-reply-button').click
+ end
+
+ it_behaves_like 'no email participants warning', '.note-edit-form'
+ end
end
end
diff --git a/spec/features/projects/issues/viewing_relocated_issues_spec.rb b/spec/features/projects/issues/viewing_relocated_issues_spec.rb
index 10d5ad1747c..abd36b3ceef 100644
--- a/spec/features/projects/issues/viewing_relocated_issues_spec.rb
+++ b/spec/features/projects/issues/viewing_relocated_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'issues canonical link' do
+RSpec.describe 'issues canonical link', feature_category: :team_planning do
include Spec::Support::Helpers::Features::CanonicalLinkHelpers
let_it_be(:original_project) { create(:project, :public) }
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index 740d009d6b8..c3c0043a6ef 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Jobs Permissions' do
+RSpec.describe 'Project Jobs Permissions', feature_category: :projects do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:group) { create(:group, name: 'some group') }
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index 6a0cfcde812..78fb72ad2df 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User browses a job', :js do
+RSpec.describe 'User browses a job', :js, feature_category: :projects do
include Spec::Support::Helpers::ModalHelpers
let(:user) { create(:user) }
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index cb3c1594868..1634f6dee74 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -8,7 +8,7 @@ def visit_jobs_page
wait_for_requests
end
-RSpec.describe 'User browses jobs' do
+RSpec.describe 'User browses jobs', feature_category: :projects do
describe 'Jobs', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb b/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb
index eea7e070a35..a9e0fce1a1c 100644
--- a/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb
+++ b/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User triggers manual job with variables', :js do
+RSpec.describe 'User triggers manual job with variables', :js, feature_category: :projects do
let(:user) { create(:user) }
let(:user_access_level) { :developer }
let(:project) { create(:project, :repository, namespace: user.namespace) }
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 96a8168e708..557a20ff2d6 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'tempfile'
-RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
+RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :projects do
include Gitlab::Routing
include ProjectForksHelper
@@ -215,10 +215,6 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
visit project_job_path(project, job)
end
- it 'shows retry button' do
- expect(page).to have_link('Retry')
- end
-
context 'if job passed' do
it 'does not show New issue button' do
expect(page).not_to have_link('New issue')
diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
index 91a30004fc3..846a0a25891 100644
--- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Issue prioritization' do
+RSpec.describe 'Issue prioritization', feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
diff --git a/spec/features/projects/labels/search_labels_spec.rb b/spec/features/projects/labels/search_labels_spec.rb
index 04dfd4ca5f1..d058565925e 100644
--- a/spec/features/projects/labels/search_labels_spec.rb
+++ b/spec/features/projects/labels/search_labels_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Search for labels', :js do
+RSpec.describe 'Search for labels', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project) }
let!(:label1) { create(:label, title: 'Foo', description: 'Lorem ipsum', project: project) }
diff --git a/spec/features/projects/labels/sort_labels_spec.rb b/spec/features/projects/labels/sort_labels_spec.rb
index f2f1acd2348..378a575348e 100644
--- a/spec/features/projects/labels/sort_labels_spec.rb
+++ b/spec/features/projects/labels/sort_labels_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Sort labels', :js do
+RSpec.describe 'Sort labels', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project) }
let!(:label1) { create(:label, title: 'Foo', description: 'Lorem ipsum', project: project) }
diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb
index 7ca8a542c21..f1537458a18 100644
--- a/spec/features/projects/labels/subscription_spec.rb
+++ b/spec/features/projects/labels/subscription_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Labels subscription' do
+RSpec.describe 'Labels subscription', feature_category: :team_planning do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :public, namespace: group) }
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 706ea92c086..b527b8926a0 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Prioritize labels' do
+RSpec.describe 'Prioritize labels', feature_category: :team_planning do
include DragTo
let(:user) { create(:user) }
diff --git a/spec/features/projects/labels/user_creates_labels_spec.rb b/spec/features/projects/labels/user_creates_labels_spec.rb
index 001d23cd2c9..46729048fe7 100644
--- a/spec/features/projects/labels/user_creates_labels_spec.rb
+++ b/spec/features/projects/labels/user_creates_labels_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User creates labels" do
+RSpec.describe "User creates labels", feature_category: :team_planning do
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/labels/user_edits_labels_spec.rb b/spec/features/projects/labels/user_edits_labels_spec.rb
index 999c238c7b3..f90f215f9fc 100644
--- a/spec/features/projects/labels/user_edits_labels_spec.rb
+++ b/spec/features/projects/labels/user_edits_labels_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User edits labels" do
+RSpec.describe "User edits labels", feature_category: :team_planning do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/projects/labels/user_promotes_label_spec.rb b/spec/features/projects/labels/user_promotes_label_spec.rb
index 4cb22c2e48c..e130dc561da 100644
--- a/spec/features/projects/labels/user_promotes_label_spec.rb
+++ b/spec/features/projects/labels/user_promotes_label_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User promotes label' do
+RSpec.describe 'User promotes label', feature_category: :team_planning do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: group) }
diff --git a/spec/features/projects/labels/user_removes_labels_spec.rb b/spec/features/projects/labels/user_removes_labels_spec.rb
index 11d73a56965..55dc52b8ccf 100644
--- a/spec/features/projects/labels/user_removes_labels_spec.rb
+++ b/spec/features/projects/labels/user_removes_labels_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User removes labels" do
+RSpec.describe "User removes labels", feature_category: :team_planning do
let(:project) { create(:project_empty_repo, :public) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb b/spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb
index f9c65c08ec0..117371e6904 100644
--- a/spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb
+++ b/spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'New project label breadcrumb' do
+RSpec.describe 'New project label breadcrumb', feature_category: :team_planning do
let(:project) { create(:project) }
let(:user) { project.creator }
diff --git a/spec/features/projects/labels/user_sees_links_to_issuables_spec.rb b/spec/features/projects/labels/user_sees_links_to_issuables_spec.rb
index 6f98883a412..d8c673a2ce5 100644
--- a/spec/features/projects/labels/user_sees_links_to_issuables_spec.rb
+++ b/spec/features/projects/labels/user_sees_links_to_issuables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Labels > User sees links to issuables' do
+RSpec.describe 'Projects > Labels > User sees links to issuables', feature_category: :team_planning do
let_it_be(:user) { create(:user) }
before do
diff --git a/spec/features/projects/labels/user_views_labels_spec.rb b/spec/features/projects/labels/user_views_labels_spec.rb
index 7a6942b6259..b5a9a735af9 100644
--- a/spec/features/projects/labels/user_views_labels_spec.rb
+++ b/spec/features/projects/labels/user_views_labels_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User views labels" do
+RSpec.describe "User views labels", feature_category: :team_planning do
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
index c9fee9bee7a..63dc99efc8f 100644
--- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
+++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Group member cannot leave group project' do
+RSpec.describe 'Projects > Members > Group member cannot leave group project', feature_category: :subgroups do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
diff --git a/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
index 34c870b8a96..07886950b95 100644
--- a/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
+++ b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Group member cannot request access to their group project' do
+RSpec.describe 'Projects > Members > Group member cannot request access to their group project',
+feature_category: :subgroups do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
index 6aa6acbdae4..416b96ab668 100644
--- a/spec/features/projects/members/group_members_spec.rb
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects members', :js do
+RSpec.describe 'Projects members', :js, feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
let(:user) { create(:user) }
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
index ec86b7db4fa..7a11ee61c5f 100644
--- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Group requester cannot request access to project', :js do
+RSpec.describe 'Projects > Members > Group requester cannot request access to project', :js,
+feature_category: :subgroups do
let(:user) { create(:user) }
let(:owner) { create(:user) }
let(:group) { create(:group, :public) }
diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
index 821b9249aa8..51acba246c5 100644
--- a/spec/features/projects/members/groups_with_access_list_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Groups with access list', :js do
+RSpec.describe 'Projects > Members > Groups with access list', :js, feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::ModalHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
diff --git a/spec/features/projects/members/manage_groups_spec.rb b/spec/features/projects/members/manage_groups_spec.rb
index e86affbbca1..b78bfacf171 100644
--- a/spec/features/projects/members/manage_groups_spec.rb
+++ b/spec/features/projects/members/manage_groups_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project > Members > Manage groups', :js do
+RSpec.describe 'Project > Members > Manage groups', :js, feature_category: :subgroups do
include ActionView::Helpers::DateHelper
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
diff --git a/spec/features/projects/members/manage_members_spec.rb b/spec/features/projects/members/manage_members_spec.rb
index 1f317c55256..3ffa402dc2c 100644
--- a/spec/features/projects/members/manage_members_spec.rb
+++ b/spec/features/projects/members/manage_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Manage members', :js, product_group: :onboarding do
+RSpec.describe 'Projects > Members > Manage members', :js, feature_category: :onboarding do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
include Spec::Support::Helpers::ModalHelpers
@@ -104,7 +104,6 @@ RSpec.describe 'Projects > Members > Manage members', :js, product_group: :onboa
click_on 'Invite members'
- click_on 'Guest'
wait_for_requests
end
@@ -112,13 +111,7 @@ RSpec.describe 'Projects > Members > Manage members', :js, product_group: :onboa
let(:current_user) { project_owner }
it 'shows Owner in the dropdown' do
- page.within '.dropdown-menu' do
- expect(page).to have_button('Guest')
- expect(page).to have_button('Reporter')
- expect(page).to have_button('Developer')
- expect(page).to have_button('Maintainer')
- expect(page).to have_button('Owner')
- end
+ expect(page).to have_select('Select a role', options: %w[Guest Reporter Developer Maintainer Owner])
end
end
@@ -126,13 +119,8 @@ RSpec.describe 'Projects > Members > Manage members', :js, product_group: :onboa
let(:current_user) { project_maintainer }
it 'does not show the Owner option' do
- page.within '.dropdown-menu' do
- expect(page).to have_button('Guest')
- expect(page).to have_button('Reporter')
- expect(page).to have_button('Developer')
- expect(page).to have_button('Maintainer')
- expect(page).not_to have_button('Owner')
- end
+ expect(page).to have_select('Select a role', options: %w[Guest Reporter Developer Maintainer])
+ expect(page).not_to have_select('Select a role', options: %w[Owner])
end
end
end
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index c92e8bc2954..31c8237aacc 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Maintainer adds member with expiration date', :js do
+RSpec.describe 'Projects > Members > Maintainer adds member with expiration date', :js, feature_category: :subgroups do
include ActiveSupport::Testing::TimeHelpers
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index f4e8c55e3cc..cea59679226 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Maintainer manages access requests' do
+RSpec.describe 'Projects > Members > Maintainer manages access requests', feature_category: :subgroups do
it_behaves_like 'Maintainer manages access requests' do
let(:entity) { create(:project, :public, :with_namespace_settings) }
let(:members_page_path) { project_project_members_path(entity) }
diff --git a/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb
index fa02e815867..dc18ca88c36 100644
--- a/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb
+++ b/spec/features/projects/members/member_cannot_request_access_to_his_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Member cannot request access to their project' do
+RSpec.describe 'Projects > Members > Member cannot request access to their project', feature_category: :subgroups do
let(:member) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
index db227f3701d..2632bc2f5bd 100644
--- a/spec/features/projects/members/member_leaves_project_spec.rb
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Member leaves project' do
+RSpec.describe 'Projects > Members > Member leaves project', feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::ModalHelpers
diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
index 45a8f979b87..7908fd3a98f 100644
--- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Owner cannot leave project' do
+RSpec.describe 'Projects > Members > Owner cannot leave project', feature_category: :subgroups do
let(:project) { create(:project) }
before do
diff --git a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
index fad5d831c19..b5a862578d3 100644
--- a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Owner cannot request access to their own project' do
+RSpec.describe 'Projects > Members > Owner cannot request access to their own project', feature_category: :subgroups do
let(:project) { create(:project) }
before do
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index 8aadd6302d0..5c72d9efeb3 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Sorting', :js do
+RSpec.describe 'Projects > Members > Sorting', :js, feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
let(:maintainer) { create(:user, name: 'John Doe', created_at: 5.days.ago, last_activity_on: Date.today) }
diff --git a/spec/features/projects/members/tabs_spec.rb b/spec/features/projects/members/tabs_spec.rb
index 5611e7ee810..232420224fc 100644
--- a/spec/features/projects/members/tabs_spec.rb
+++ b/spec/features/projects/members/tabs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Tabs', :js do
+RSpec.describe 'Projects > Members > Tabs', :js, feature_category: :subgroups do
include Spec::Support::Helpers::Features::MembersHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index be124502c32..11d162fabd4 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > User requests access', :js do
+RSpec.describe 'Projects > Members > User requests access', :js, feature_category: :subgroups do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
index eb52a7821f9..56aee469252 100644
--- a/spec/features/projects/merge_request_button_spec.rb
+++ b/spec/features/projects/merge_request_button_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Request button' do
+RSpec.describe 'Merge Request button', feature_category: :projects do
include ProjectForksHelper
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/milestones/gfm_autocomplete_spec.rb b/spec/features/projects/milestones/gfm_autocomplete_spec.rb
index 547a5d11dec..d4ce10b5cb5 100644
--- a/spec/features/projects/milestones/gfm_autocomplete_spec.rb
+++ b/spec/features/projects/milestones/gfm_autocomplete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'GFM autocomplete', :js do
+RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:group) { create(:group, name: 'Ancestor') }
let_it_be(:project) { create(:project, :repository, group: group) }
diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb
index 6bd139c0ebe..73d46d3764e 100644
--- a/spec/features/projects/milestones/milestone_spec.rb
+++ b/spec/features/projects/milestones/milestone_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project milestone', :js do
+RSpec.describe 'Project milestone', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:milestone) { create(:milestone, project: project) }
diff --git a/spec/features/projects/milestones/milestones_sorting_spec.rb b/spec/features/projects/milestones/milestones_sorting_spec.rb
index 5ba4289fd11..8a8e7d07435 100644
--- a/spec/features/projects/milestones/milestones_sorting_spec.rb
+++ b/spec/features/projects/milestones/milestones_sorting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Milestones sorting', :js do
+RSpec.describe 'Milestones sorting', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:milestones_for_sort_by) do
diff --git a/spec/features/projects/milestones/new_spec.rb b/spec/features/projects/milestones/new_spec.rb
index 170268297cd..ef9325a1627 100644
--- a/spec/features/projects/milestones/new_spec.rb
+++ b/spec/features/projects/milestones/new_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creating a new project milestone', :js do
+RSpec.describe 'Creating a new project milestone', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
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 d658599c52b..36dfee7811d 100644
--- a/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
+++ b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User interacts with labels' do
+RSpec.describe 'User interacts with labels', feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:milestone) { create(:milestone, project: project, title: 'v2.2', description: '# Description header') }
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index 5b5f7860e43..4d85b5cfb2e 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project navbar' do
+RSpec.describe 'Project navbar', feature_category: :projects do
include NavbarStructureHelper
include WaitForRequests
diff --git a/spec/features/projects/network_graph_spec.rb b/spec/features/projects/network_graph_spec.rb
index 97b743b4d73..b36fde8a2bf 100644
--- a/spec/features/projects/network_graph_spec.rb
+++ b/spec/features/projects/network_graph_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Network Graph', :js do
+RSpec.describe 'Project Network Graph', :js, feature_category: :projects do
let(:user) { create :user }
let(:project) { create :project, :repository, namespace: user.namespace }
diff --git a/spec/features/projects/new_project_from_template_spec.rb b/spec/features/projects/new_project_from_template_spec.rb
index 1c8647d859a..97304840010 100644
--- a/spec/features/projects/new_project_from_template_spec.rb
+++ b/spec/features/projects/new_project_from_template_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'New project from template', :js do
+RSpec.describe 'New project from template', :js, feature_category: :projects do
let(:user) { create(:user) }
before do
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 7cf05242a23..769ad5bf61a 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'New project', :js do
+RSpec.describe 'New project', :js, feature_category: :projects do
include Spec::Support::Helpers::Features::TopNavSpecHelpers
context 'as a user' do
diff --git a/spec/features/projects/package_files_spec.rb b/spec/features/projects/package_files_spec.rb
index 6dc0294bb9e..824b57db7ad 100644
--- a/spec/features/projects/package_files_spec.rb
+++ b/spec/features/projects/package_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'PackageFiles' do
+RSpec.describe 'PackageFiles', feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project) }
let!(:package) { create(:maven_package, project: project) }
diff --git a/spec/features/projects/packages_spec.rb b/spec/features/projects/packages_spec.rb
index bbe913cf1e5..31ff455d0df 100644
--- a/spec/features/projects/packages_spec.rb
+++ b/spec/features/projects/packages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Packages' do
+RSpec.describe 'Packages', feature_category: :projects do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/features/projects/pages/user_adds_domain_spec.rb b/spec/features/projects/pages/user_adds_domain_spec.rb
index 5cb4fa163c8..708210e669c 100644
--- a/spec/features/projects/pages/user_adds_domain_spec.rb
+++ b/spec/features/projects/pages/user_adds_domain_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'User adds pages domain', :js do
+RSpec.describe 'User adds pages domain', :js, feature_category: :pages do
include LetsEncryptHelpers
include Spec::Support::Helpers::ModalHelpers
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 029479d6b95..baef75ca303 100644
--- a/spec/features/projects/pages/user_configures_pages_pipeline_spec.rb
+++ b/spec/features/projects/pages/user_configures_pages_pipeline_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Pages edits pages settings', :js do
+RSpec.describe 'Pages edits pages settings', :js, feature_category: :pages do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:project) { create(:project, pages_https_only: false) }
@@ -46,14 +46,14 @@ RSpec.describe 'Pages edits pages settings', :js do
Feature.disable(:use_pipeline_wizard_for_pages)
end
+ after do
+ Feature.enable(:use_pipeline_wizard_for_pages)
+ end
+
it 'shows configure pages instructions' do
visit project_pages_path(project)
expect(page).to have_content('Configure pages')
end
-
- after do
- Feature.enable(:use_pipeline_wizard_for_pages)
- end
end
end
diff --git a/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb b/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb
index 2e28fa20b90..a7da59200e9 100644
--- a/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb
+++ b/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe "Pages with Let's Encrypt", :https_pages_enabled do
+RSpec.describe "Pages with Let's Encrypt", :https_pages_enabled, feature_category: :pages do
include LetsEncryptHelpers
include Spec::Support::Helpers::ModalHelpers
diff --git a/spec/features/projects/pages/user_edits_settings_spec.rb b/spec/features/projects/pages/user_edits_settings_spec.rb
index 88c27a6adf2..7ceefdecbae 100644
--- a/spec/features/projects/pages/user_edits_settings_spec.rb
+++ b/spec/features/projects/pages/user_edits_settings_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Pages edits pages settings', :js do
+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) }
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index e569fef76f8..8beb8af1a8e 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Pipeline Schedules', :js do
+RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
include Spec::Support::Helpers::ModalHelpers
let!(:project) { create(:project, :repository) }
@@ -64,7 +64,7 @@ RSpec.describe 'Pipeline Schedules', :js do
it 'shows the pipeline schedule with default ref' do
page.within('[data-testid="schedule-target-ref"]') do
- expect(first('.gl-new-dropdown-button-text').text).to eq('master')
+ expect(first('.gl-dropdown-button-text').text).to eq('master')
end
end
end
@@ -77,7 +77,7 @@ RSpec.describe 'Pipeline Schedules', :js do
it 'shows the pipeline schedule with default ref' do
page.within('[data-testid="schedule-target-ref"]') do
- expect(first('.gl-new-dropdown-button-text').text).to eq('master')
+ expect(first('.gl-dropdown-button-text').text).to eq('master')
end
end
end
diff --git a/spec/features/projects/pipelines/legacy_pipeline_spec.rb b/spec/features/projects/pipelines/legacy_pipeline_spec.rb
deleted file mode 100644
index c4fc194f0cd..00000000000
--- a/spec/features/projects/pipelines/legacy_pipeline_spec.rb
+++ /dev/null
@@ -1,1315 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Pipeline', :js do
- include RoutesHelpers
- include ProjectForksHelper
- include ::ExclusiveLeaseHelpers
-
- let_it_be(:project) { create(:project) }
-
- let(:user) { create(:user) }
- let(:role) { :developer }
-
- before do
- sign_in(user)
- project.add_role(user, role)
- stub_feature_flags(pipeline_tabs_vue: false)
- end
-
- shared_context 'pipeline builds' do
- let!(:build_passed) do
- create(:ci_build, :success,
- pipeline: pipeline, stage: 'build', stage_idx: 0, name: 'build')
- end
-
- let!(:build_failed) do
- create(:ci_build, :failed,
- pipeline: pipeline, stage: 'test', stage_idx: 1, name: 'test')
- end
-
- let!(:build_preparing) do
- create(:ci_build, :preparing,
- pipeline: pipeline, stage: 'deploy', stage_idx: 2, name: 'prepare')
- end
-
- let!(:build_running) do
- create(:ci_build, :running,
- pipeline: pipeline, stage: 'deploy', stage_idx: 3, name: 'deploy')
- end
-
- let!(:build_manual) do
- create(:ci_build, :manual,
- pipeline: pipeline, stage: 'deploy', stage_idx: 3, name: 'manual-build')
- end
-
- let!(:build_scheduled) do
- create(:ci_build, :scheduled,
- pipeline: pipeline, stage: 'deploy', stage_idx: 3, name: 'delayed-job')
- end
-
- let!(:build_external) do
- create(:generic_commit_status, status: 'success',
- pipeline: pipeline,
- name: 'jenkins',
- stage: 'external',
- ref: 'master',
- target_url: 'http://gitlab.com/status')
- end
- end
-
- describe 'GET /:project/-/pipelines/:id' do
- include_context 'pipeline builds'
-
- let_it_be(:group) { create(:group) }
- let_it_be(:project, reload: true) { create(:project, :repository, group: group) }
-
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) }
-
- subject(:visit_pipeline) { visit project_pipeline_path(project, pipeline) }
-
- it 'shows the pipeline graph' do
- visit_pipeline
-
- expect(page).to have_selector('.js-pipeline-graph')
- expect(page).to have_content('build')
- expect(page).to have_content('test')
- expect(page).to have_content('deploy')
- expect(page).to have_content('Retry')
- expect(page).to have_content('Cancel running')
- end
-
- it 'shows Pipeline tab pane as active' do
- visit_pipeline
-
- expect(page).to have_css('#js-tab-pipeline.active')
- end
-
- it 'shows link to the pipeline ref' do
- visit_pipeline
-
- expect(page).to have_link(pipeline.ref)
- end
-
- it 'shows the pipeline information' do
- visit_pipeline
-
- within '.pipeline-info' do
- expect(page).to have_content("#{pipeline.statuses.count} jobs " \
- "for #{pipeline.ref}")
- expect(page).to have_link(pipeline.ref,
- href: project_commits_path(pipeline.project, pipeline.ref))
- end
- end
-
- describe 'related merge requests' do
- context 'when there are no related merge requests' do
- it 'shows a "no related merge requests" message' do
- visit_pipeline
-
- within '.related-merge-request-info' do
- expect(page).to have_content('No related merge requests found.')
- end
- end
- end
-
- context 'when there is one related merge request' do
- let!(:merge_request) do
- create(:merge_request,
- source_project: project,
- source_branch: pipeline.ref)
- end
-
- it 'shows a link to the merge request' do
- visit_pipeline
-
- within '.related-merge-requests' do
- expect(page).to have_content('1 related merge request: ')
- expect(page).to have_selector('.js-truncated-mr-list')
- expect(page).to have_link("#{merge_request.to_reference} #{merge_request.title}")
-
- expect(page).not_to have_selector('.js-full-mr-list')
- expect(page).not_to have_selector('.text-expander')
- end
- end
- end
-
- context 'when there are two related merge requests' do
- let!(:merge_request1) do
- create(:merge_request,
- source_project: project,
- source_branch: pipeline.ref)
- end
-
- let!(:merge_request2) do
- create(:merge_request,
- source_project: project,
- source_branch: pipeline.ref,
- target_branch: 'fix')
- end
-
- it 'links to the most recent related merge request' do
- visit_pipeline
-
- within '.related-merge-requests' do
- expect(page).to have_content('2 related merge requests: ')
- expect(page).to have_link("#{merge_request2.to_reference} #{merge_request2.title}")
- expect(page).to have_selector('.text-expander')
- expect(page).to have_selector('.js-full-mr-list', visible: false)
- end
- end
-
- it 'expands to show links to all related merge requests' do
- visit_pipeline
-
- within '.related-merge-requests' do
- find('.text-expander').click
-
- expect(page).to have_selector('.js-full-mr-list', visible: true)
-
- pipeline.all_merge_requests.map do |merge_request|
- expect(page).to have_link(href: project_merge_request_path(project, merge_request))
- end
- end
- end
- end
- end
-
- describe 'pipelines details view' do
- let!(:status) { create(:user_status, user: pipeline.user, emoji: 'smirk', message: 'Authoring this object') }
-
- it 'pipeline header shows the user status and emoji' do
- visit project_pipeline_path(project, pipeline)
-
- within '[data-testid="ci-header-content"]' do
- expect(page).to have_selector("[data-testid='#{status.message}']")
- expect(page).to have_selector("[data-name='#{status.emoji}']")
- end
- end
- end
-
- describe 'pipeline graph' do
- before do
- visit_pipeline
- end
-
- context 'when pipeline has running builds' do
- it 'shows a running icon and a cancel action for the running build' do
- page.within('#ci-badge-deploy') do
- expect(page).to have_selector('.js-ci-status-icon-running')
- expect(page).to have_selector('.js-icon-cancel')
- expect(page).to have_content('deploy')
- end
- end
-
- it 'cancels the running build and shows retry button', :sidekiq_might_not_need_inline do
- find('#ci-badge-deploy .ci-action-icon-container').click
-
- page.within('#ci-badge-deploy') do
- expect(page).to have_css('.js-icon-retry')
- end
- end
- end
-
- context 'when pipeline has preparing builds' do
- it 'shows a preparing icon and a cancel action' do
- page.within('#ci-badge-prepare') do
- expect(page).to have_selector('.js-ci-status-icon-preparing')
- expect(page).to have_selector('.js-icon-cancel')
- expect(page).to have_content('prepare')
- end
- end
-
- it 'cancels the preparing build and shows retry button', :sidekiq_might_not_need_inline do
- find('#ci-badge-deploy .ci-action-icon-container').click
-
- page.within('#ci-badge-deploy') do
- expect(page).to have_css('.js-icon-retry')
- end
- end
- end
-
- context 'when pipeline has successful builds' do
- it 'shows the success icon and a retry action for the successful build' do
- page.within('#ci-badge-build') do
- expect(page).to have_selector('.js-ci-status-icon-success')
- expect(page).to have_content('build')
- end
-
- page.within('#ci-badge-build .ci-action-icon-container.js-icon-retry') do
- expect(page).to have_selector('svg')
- end
- end
-
- it 'is possible to retry the success job', :sidekiq_might_not_need_inline do
- find('#ci-badge-build .ci-action-icon-container').click
- wait_for_requests
-
- expect(page).not_to have_content('Retry job')
- within('.js-pipeline-header-container') do
- expect(page).to have_selector('.js-ci-status-icon-running')
- end
- end
- end
-
- context 'when pipeline has a delayed job' do
- let(:project) { create(:project, :repository, group: group) }
-
- it 'shows the scheduled icon and an unschedule action for the delayed job' do
- page.within('#ci-badge-delayed-job') do
- expect(page).to have_selector('.js-ci-status-icon-scheduled')
- expect(page).to have_content('delayed-job')
- end
-
- page.within('#ci-badge-delayed-job .ci-action-icon-container.js-icon-time-out') do
- expect(page).to have_selector('svg')
- end
- end
-
- it 'unschedules the delayed job and shows play button as a manual job', :sidekiq_might_not_need_inline do
- find('#ci-badge-delayed-job .ci-action-icon-container').click
-
- page.within('#ci-badge-delayed-job') do
- expect(page).to have_css('.js-icon-play')
- end
- end
- end
-
- context 'when pipeline has failed builds' do
- it 'shows the failed icon and a retry action for the failed build' do
- page.within('#ci-badge-test') do
- expect(page).to have_selector('.js-ci-status-icon-failed')
- expect(page).to have_content('test')
- end
-
- page.within('#ci-badge-test .ci-action-icon-container.js-icon-retry') do
- expect(page).to have_selector('svg')
- end
- end
-
- it 'is possible to retry the failed build', :sidekiq_might_not_need_inline do
- find('#ci-badge-test .ci-action-icon-container').click
- wait_for_requests
-
- expect(page).not_to have_content('Retry job')
- within('.js-pipeline-header-container') do
- expect(page).to have_selector('.js-ci-status-icon-running')
- end
- end
-
- it 'includes the failure reason' do
- page.within('#ci-badge-test') do
- build_link = page.find('.js-pipeline-graph-job-link')
- expect(build_link['title']).to eq('test - failed - (unknown failure)')
- end
- end
- end
-
- context 'when pipeline has manual jobs' do
- it 'shows the skipped icon and a play action for the manual build' do
- page.within('#ci-badge-manual-build') do
- expect(page).to have_selector('.js-ci-status-icon-manual')
- expect(page).to have_content('manual')
- end
-
- page.within('#ci-badge-manual-build .ci-action-icon-container.js-icon-play') do
- expect(page).to have_selector('svg')
- end
- end
-
- it 'is possible to play the manual job', :sidekiq_might_not_need_inline do
- find('#ci-badge-manual-build .ci-action-icon-container').click
- wait_for_requests
-
- expect(page).not_to have_content('Play job')
- within('.js-pipeline-header-container') do
- expect(page).to have_selector('.js-ci-status-icon-running')
- end
- end
- end
-
- context 'when pipeline has external job' do
- it 'shows the success icon and the generic comit status build' do
- expect(page).to have_selector('.js-ci-status-icon-success')
- expect(page).to have_content('jenkins')
- expect(page).to have_link('jenkins', href: 'http://gitlab.com/status')
- end
- end
- end
-
- context 'when the pipeline has manual stage' do
- before do
- create(:ci_build, :manual, pipeline: pipeline, stage_idx: 10, stage: 'publish', name: 'CentOS')
- create(:ci_build, :manual, pipeline: pipeline, stage_idx: 10, stage: 'publish', name: 'Debian')
- create(:ci_build, :manual, pipeline: pipeline, stage_idx: 10, stage: 'publish', name: 'OpenSUDE')
-
- # force to update stages statuses
- Ci::ProcessPipelineService.new(pipeline).execute
-
- visit_pipeline
- end
-
- it 'displays play all button' do
- expect(page).to have_selector('.js-stage-action')
- end
- end
-
- context 'page tabs' do
- before do
- visit_pipeline
- end
-
- it 'shows Pipeline, Jobs, DAG and Failed Jobs tabs with link' do
- expect(page).to have_link('Pipeline')
- expect(page).to have_link('Jobs')
- expect(page).to have_link('Needs')
- expect(page).to have_link('Failed Jobs')
- end
-
- it 'shows counter in Jobs tab' do
- expect(page.find('.js-builds-counter').text).to eq(pipeline.total_size.to_s)
- end
-
- it 'shows Pipeline tab as active' do
- expect(page).to have_css('.js-pipeline-tab-link .active')
- end
-
- context 'without permission to access builds' do
- let(:project) { create(:project, :public, :repository, public_builds: false) }
- let(:role) { :guest }
-
- it 'does not show the pipeline details page' do
- expect(page).to have_content('Not Found')
- end
- end
- end
-
- describe 'test tabs' do
- let(:pipeline) { create(:ci_pipeline, :with_test_reports, :with_report_results, project: project) }
-
- before do
- stub_feature_flags(pipeline_tabs_vue: false)
- visit_pipeline
- wait_for_requests
- end
-
- context 'with test reports' do
- it 'shows badge counter in Tests tab' do
- expect(page.find('.js-test-report-badge-counter').text).to eq(pipeline.test_report_summary.total[:count].to_s)
- end
-
- it 'calls summary.json endpoint', :js do
- find('.js-tests-tab-link').click
-
- expect(page).to have_content('Jobs')
- expect(page).to have_selector('[data-testid="tests-detail"]', visible: :all)
- end
- end
-
- context 'without test reports' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
-
- it 'shows zero' do
- expect(page.find('.js-test-report-badge-counter', visible: :all).text).to eq("0")
- end
- end
- end
-
- context 'retrying jobs' do
- before do
- visit_pipeline
- end
-
- it { expect(page).not_to have_content('retried') }
-
- context 'when retrying' do
- before do
- find('[data-testid="retryPipeline"]').click
- wait_for_requests
- end
-
- it 'does not show a "Retry" button', :sidekiq_might_not_need_inline do
- expect(page).not_to have_content('Retry')
- end
-
- it 'shows running status in pipeline header', :sidekiq_might_not_need_inline do
- within('.js-pipeline-header-container') do
- expect(page).to have_selector('.js-ci-status-icon-running')
- end
- end
- end
- end
-
- context 'canceling jobs' do
- before do
- visit_pipeline
- end
-
- it { expect(page).not_to have_selector('.ci-canceled') }
-
- context 'when canceling' do
- before do
- click_on 'Cancel running'
- end
-
- it 'does not show a "Cancel running" button', :sidekiq_might_not_need_inline do
- expect(page).not_to have_content('Cancel running')
- end
- end
- end
-
- context 'when user can not delete' do
- before do
- visit_pipeline
- end
-
- it { expect(page).not_to have_button('Delete') }
- end
-
- context 'when deleting' do
- before do
- group.add_owner(user)
-
- visit_pipeline
-
- click_button 'Delete'
- click_button 'Delete pipeline'
- end
-
- it 'redirects to pipeline overview page', :sidekiq_inline do
- expect(page).to have_content('The pipeline has been deleted')
- expect(page).to have_current_path(project_pipelines_path(project), ignore_query: true)
- end
- end
-
- context 'when pipeline ref does not exist in repository anymore' do
- let(:pipeline) do
- create(:ci_empty_pipeline, project: project,
- ref: 'non-existent',
- sha: project.commit.id,
- user: user)
- end
-
- before do
- visit_pipeline
- end
-
- it 'does not render link to the pipeline ref' do
- expect(page).not_to have_link(pipeline.ref)
- expect(page).to have_content(pipeline.ref)
- end
-
- it 'does not render render raw HTML to the pipeline ref' do
- page.within '.pipeline-info' do
- expect(page).not_to have_content('<span class="ref-name"')
- end
- end
- end
-
- context 'when pipeline is detached merge request pipeline' do
- let(:source_project) { project }
- let(:target_project) { project }
-
- let(:merge_request) do
- create(:merge_request,
- :with_detached_merge_request_pipeline,
- source_project: source_project,
- target_project: target_project)
- end
-
- let(:pipeline) do
- merge_request.all_pipelines.last
- end
-
- it 'shows the pipeline information' do
- visit_pipeline
-
- within '.pipeline-info' do
- expect(page).to have_content("#{pipeline.statuses.count} jobs " \
- "for !#{merge_request.iid} " \
- "with #{merge_request.source_branch}")
- expect(page).to have_link("!#{merge_request.iid}",
- href: project_merge_request_path(project, merge_request))
- expect(page).to have_link(merge_request.source_branch,
- href: project_commits_path(merge_request.source_project, merge_request.source_branch))
- end
- end
-
- context 'when source branch does not exist' do
- before do
- project.repository.rm_branch(user, merge_request.source_branch)
- end
-
- it 'does not link to the source branch commit path' do
- visit_pipeline
-
- within '.pipeline-info' do
- expect(page).not_to have_link(merge_request.source_branch)
- expect(page).to have_content(merge_request.source_branch)
- end
- end
- end
-
- context 'when source project is a forked project' do
- let(:source_project) { fork_project(project, user, repository: true) }
-
- before do
- visit project_pipeline_path(source_project, pipeline)
- end
-
- it 'shows the pipeline information', :sidekiq_might_not_need_inline do
- within '.pipeline-info' do
- expect(page).to have_content("#{pipeline.statuses.count} jobs " \
- "for !#{merge_request.iid} " \
- "with #{merge_request.source_branch}")
- expect(page).to have_link("!#{merge_request.iid}",
- href: project_merge_request_path(project, merge_request))
- expect(page).to have_link(merge_request.source_branch,
- href: project_commits_path(merge_request.source_project, merge_request.source_branch))
- end
- end
- end
- end
-
- context 'when pipeline is merge request pipeline' do
- let(:project) { create(:project, :repository, group: group) }
- let(:source_project) { project }
- let(:target_project) { project }
-
- let(:merge_request) do
- create(:merge_request,
- :with_merge_request_pipeline,
- source_project: source_project,
- target_project: target_project,
- merge_sha: project.commit.id)
- end
-
- let(:pipeline) do
- merge_request.all_pipelines.last
- end
-
- before do
- pipeline.update!(user: user)
- end
-
- it 'shows the pipeline information' do
- visit_pipeline
-
- within '.pipeline-info' do
- expect(page).to have_content("#{pipeline.statuses.count} jobs " \
- "for !#{merge_request.iid} " \
- "with #{merge_request.source_branch} " \
- "into #{merge_request.target_branch}")
- expect(page).to have_link("!#{merge_request.iid}",
- href: project_merge_request_path(project, merge_request))
- expect(page).to have_link(merge_request.source_branch,
- href: project_commits_path(merge_request.source_project, merge_request.source_branch))
- expect(page).to have_link(merge_request.target_branch,
- href: project_commits_path(merge_request.target_project, merge_request.target_branch))
- end
- end
-
- context 'when target branch does not exist' do
- before do
- project.repository.rm_branch(user, merge_request.target_branch)
- end
-
- it 'does not link to the target branch commit path' do
- visit_pipeline
-
- within '.pipeline-info' do
- expect(page).not_to have_link(merge_request.target_branch)
- expect(page).to have_content(merge_request.target_branch)
- end
- end
- end
-
- context 'when source project is a forked project' do
- let(:source_project) { fork_project(project, user, repository: true) }
-
- before do
- visit project_pipeline_path(source_project, pipeline)
- end
-
- it 'shows the pipeline information', :sidekiq_might_not_need_inline do
- within '.pipeline-info' do
- expect(page).to have_content("#{pipeline.statuses.count} jobs " \
- "for !#{merge_request.iid} " \
- "with #{merge_request.source_branch} " \
- "into #{merge_request.target_branch}")
- expect(page).to have_link("!#{merge_request.iid}",
- href: project_merge_request_path(project, merge_request))
- expect(page).to have_link(merge_request.source_branch,
- href: project_commits_path(merge_request.source_project, merge_request.source_branch))
- expect(page).to have_link(merge_request.target_branch,
- href: project_commits_path(merge_request.target_project, merge_request.target_branch))
- end
- end
- end
- end
- end
-
- context 'when user does not have access to read jobs' do
- before do
- project.update!(public_builds: false)
- end
-
- describe 'GET /:project/-/pipelines/:id' do
- include_context 'pipeline builds'
-
- let_it_be(:project) { create(:project, :repository) }
-
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) }
-
- before do
- visit project_pipeline_path(project, pipeline)
- end
-
- it 'shows the pipeline graph' do
- expect(page).to have_selector('.js-pipeline-graph')
- expect(page).to have_content('build')
- expect(page).to have_content('test')
- expect(page).to have_content('deploy')
- expect(page).to have_content('Retry')
- expect(page).to have_content('Cancel running')
- end
-
- it 'does not link to job' do
- expect(page).not_to have_selector('.js-pipeline-graph-job-link')
- end
- end
- end
-
- context 'when a bridge job exists' do
- include_context 'pipeline builds'
-
- let(:project) { create(:project, :repository) }
- let(:downstream) { create(:project, :repository) }
-
- let(:pipeline) do
- create(:ci_pipeline, project: project,
- ref: 'master',
- sha: project.commit.id,
- user: user)
- end
-
- let!(:bridge) do
- create(:ci_bridge, pipeline: pipeline,
- name: 'cross-build',
- user: user,
- downstream: downstream)
- end
-
- describe 'GET /:project/-/pipelines/:id' do
- before do
- visit project_pipeline_path(project, pipeline)
- end
-
- it 'shows the pipeline with a bridge job' do
- expect(page).to have_selector('.js-pipeline-graph')
- expect(page).to have_content('cross-build')
- end
-
- context 'when a scheduled pipeline is created by a blocked user' do
- let(:project) { create(:project, :repository) }
-
- let(:schedule) do
- create(:ci_pipeline_schedule,
- project: project,
- owner: project.first_owner,
- description: 'blocked user schedule'
- ).tap do |schedule|
- schedule.update_column(:next_run_at, 1.minute.ago)
- end
- end
-
- before do
- schedule.owner.block!
- PipelineScheduleWorker.new.perform
- end
-
- it 'displays the PipelineSchedule in an inactive state' do
- stub_feature_flags(pipeline_schedules_vue: false)
-
- visit project_pipeline_schedules_path(project)
- page.click_link('Inactive')
-
- expect(page).to have_selector('table.ci-table > tbody > tr > td', text: 'blocked user schedule')
- end
-
- it 'does not create a new Pipeline' do
- visit project_pipelines_path(project)
-
- expect(page).not_to have_selector('.ci-table')
- expect(schedule.last_pipeline).to be_nil
- end
- end
- end
-
- describe 'GET /:project/-/pipelines/:id/builds' do
- before do
- visit builds_project_pipeline_path(project, pipeline)
- end
-
- it 'shows a bridge job on a list' do
- expect(page).to have_content('cross-build')
- expect(page).to have_content(bridge.id)
- end
- end
- end
-
- context 'when build requires resource', :sidekiq_inline do
- let_it_be(:project) { create(:project, :repository) }
-
- let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:resource_group) { create(:ci_resource_group, project: project) }
-
- let!(:test_job) do
- create(:ci_build, :pending, stage: 'test', name: 'test', stage_idx: 1, pipeline: pipeline, project: project)
- end
-
- let!(:deploy_job) do
- create(:ci_build, :created,
- stage: 'deploy',
- name: 'deploy',
- stage_idx: 2,
- pipeline: pipeline,
- project: project,
- resource_group: resource_group)
- end
-
- describe 'GET /:project/-/pipelines/:id' do
- subject { visit project_pipeline_path(project, pipeline) }
-
- it 'shows deploy job as created' do
- subject
-
- within('.js-pipeline-header-container') do
- expect(page).to have_content('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')
- end
-
- within(all('[data-testid="stage-column"]')[1]) do
- expect(page).to have_content('deploy')
- expect(page).to have_css('.ci-status-icon-created')
- end
- end
- end
-
- context 'when test job succeeded' do
- before do
- test_job.success!
- end
-
- it 'shows deploy job as pending' do
- subject
-
- within('.js-pipeline-header-container') do
- expect(page).to have_content('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')
- end
-
- within(all('[data-testid="stage-column"]')[1]) do
- expect(page).to have_content('deploy')
- expect(page).to have_css('.ci-status-icon-pending')
- end
- end
- end
- end
-
- context 'when test job succeeded but there are no available resources' do
- let(:another_job) { create(:ci_build, :running, project: project, resource_group: resource_group) }
-
- before do
- resource_group.assign_resource_to(another_job)
- test_job.success!
- end
-
- it 'shows deploy job as waiting for resource' do
- subject
-
- within('.js-pipeline-header-container') do
- expect(page).to have_content('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')
- end
- end
- end
-
- context 'when resource is released from another job' do
- before do
- another_job.success!
- end
-
- it 'shows deploy job as pending' do
- subject
-
- within('.js-pipeline-header-container') do
- expect(page).to have_content('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')
- end
- end
- end
- end
-
- context 'when deploy job is a bridge to trigger a downstream pipeline' do
- let!(:deploy_job) do
- create(:ci_bridge, :created,
- stage: 'deploy',
- name: 'deploy',
- stage_idx: 2,
- pipeline: pipeline,
- project: project,
- resource_group: resource_group
- )
- end
-
- it 'shows deploy job as waiting for resource' do
- subject
-
- within('.js-pipeline-header-container') do
- expect(page).to have_content('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')
- end
- end
- end
- end
-
- context 'when deploy job is a bridge to trigger a downstream pipeline' do
- let!(:deploy_job) do
- create(:ci_bridge, :created,
- stage: 'deploy',
- name: 'deploy',
- stage_idx: 2,
- pipeline: pipeline,
- project: project,
- resource_group: resource_group
- )
- end
-
- it 'shows deploy job as waiting for resource' do
- subject
-
- within('.js-pipeline-header-container') do
- expect(page).to have_content('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')
- end
- end
- end
- end
- end
- end
- end
-
- describe 'GET /:project/-/pipelines/:id/dag' do
- include_context 'pipeline builds'
-
- let_it_be(:project) { create(:project, :repository) }
-
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
-
- before do
- visit dag_project_pipeline_path(project, pipeline)
- end
-
- it 'shows DAG tab pane as active' do
- expect(page).to have_css('#js-tab-dag.active', visible: false)
- end
-
- context 'page tabs' do
- it 'shows Pipeline, Jobs and DAG tabs with link' do
- expect(page).to have_link('Pipeline')
- expect(page).to have_link('Jobs')
- expect(page).to have_link('DAG')
- end
-
- it 'shows counter in Jobs tab' do
- expect(page.find('.js-builds-counter').text).to eq(pipeline.total_size.to_s)
- end
-
- it 'shows DAG tab as active' do
- expect(page).to have_css('li.js-dag-tab-link .active')
- end
- end
- end
-
- context 'when user sees pipeline flags in a pipeline detail page' do
- let_it_be(:project) { create(:project, :repository) }
-
- context 'when pipeline is latest' do
- include_context 'pipeline builds'
-
- let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- ref: 'master',
- sha: project.commit.id,
- user: user)
- end
-
- before do
- visit project_pipeline_path(project, pipeline)
- end
-
- it 'contains badge that indicates it is the latest build' do
- page.within(all('.well-segment')[1]) do
- expect(page).to have_content 'latest'
- end
- end
- end
-
- context 'when pipeline has configuration errors' do
- let(:pipeline) do
- create(:ci_pipeline,
- :invalid,
- project: project,
- ref: 'master',
- sha: project.commit.id,
- user: user)
- end
-
- before do
- visit project_pipeline_path(project, pipeline)
- end
-
- it 'contains badge that indicates errors' do
- page.within(all('.well-segment')[1]) do
- expect(page).to have_content 'yaml invalid'
- end
- end
-
- it 'contains badge with tooltip which contains error' do
- expect(pipeline).to have_yaml_errors
-
- page.within(all('.well-segment')[1]) do
- expect(page).to have_selector(
- %Q{span[title="#{pipeline.yaml_errors}"]})
- end
- end
-
- it 'contains badge that indicates failure reason' do
- expect(page).to have_content 'error'
- end
-
- it 'contains badge with tooltip which contains failure reason' do
- expect(pipeline.failure_reason?).to eq true
-
- page.within(all('.well-segment')[1]) do
- expect(page).to have_selector(
- %Q{span[title="#{pipeline.present.failure_reason}"]})
- end
- end
-
- it 'contains a pipeline header with title' do
- expect(page).to have_content "Pipeline ##{pipeline.id}"
- end
- end
-
- context 'when pipeline is stuck' do
- include_context 'pipeline builds'
-
- let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- ref: 'master',
- sha: project.commit.id,
- user: user)
- end
-
- before do
- create(:ci_build, :pending, pipeline: pipeline)
- visit project_pipeline_path(project, pipeline)
- end
-
- it 'contains badge that indicates being stuck' do
- page.within(all('.well-segment')[1]) do
- expect(page).to have_content 'stuck'
- end
- end
- end
-
- context 'when pipeline uses auto devops' do
- include_context 'pipeline builds'
-
- let(:project) { create(:project, :repository, auto_devops_attributes: { enabled: true }) }
- let(:pipeline) do
- create(:ci_pipeline,
- :auto_devops_source,
- project: project,
- ref: 'master',
- sha: project.commit.id,
- user: user)
- end
-
- before do
- visit project_pipeline_path(project, pipeline)
- end
-
- it 'contains badge that indicates using auto devops' do
- page.within(all('.well-segment')[1]) do
- expect(page).to have_content 'Auto DevOps'
- end
- end
- end
-
- context 'when pipeline runs in a merge request context' do
- include_context 'pipeline builds'
-
- let(:pipeline) do
- create(:ci_pipeline,
- source: :merge_request_event,
- project: merge_request.source_project,
- ref: 'feature',
- sha: merge_request.diff_head_sha,
- user: user,
- merge_request: merge_request)
- end
-
- let(:merge_request) do
- create(:merge_request,
- source_project: project,
- source_branch: 'feature',
- target_project: project,
- target_branch: 'master')
- end
-
- before do
- visit project_pipeline_path(project, pipeline)
- end
-
- it 'contains badge that indicates detached merge request pipeline' do
- page.within(all('.well-segment')[1]) do
- expect(page).to have_content 'merge request'
- end
- end
- end
- end
-
- describe 'GET /:project/-/pipelines/:id/builds' do
- include_context 'pipeline builds'
-
- let_it_be(:project) { create(:project, :repository) }
-
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
-
- before do
- visit builds_project_pipeline_path(project, pipeline)
- end
-
- it 'shows a list of jobs' do
- expect(page).to have_content('Test')
- expect(page).to have_content(build_passed.id)
- expect(page).to have_content('Deploy')
- expect(page).to have_content(build_failed.id)
- expect(page).to have_content(build_running.id)
- expect(page).to have_content(build_external.id)
- expect(page).to have_content('Retry')
- expect(page).to have_content('Cancel running')
- expect(page).to have_button('Play')
- end
-
- context 'page tabs' do
- it 'shows Pipeline, Jobs and DAG tabs with link' do
- expect(page).to have_link('Pipeline')
- expect(page).to have_link('Jobs')
- expect(page).to have_link('Needs')
- end
-
- it 'shows counter in Jobs tab' do
- expect(page.find('.js-builds-counter').text).to eq(pipeline.total_size.to_s)
- end
- end
-
- context 'retrying jobs' do
- it { expect(page).not_to have_content('retried') }
-
- context 'when retrying' do
- before do
- find('[data-testid="retry"]', match: :first).click
- end
-
- it 'does not show a "Retry" button', :sidekiq_might_not_need_inline do
- expect(page).not_to have_content('Retry')
- end
- end
- end
-
- context 'canceling jobs' do
- it { expect(page).not_to have_selector('.ci-canceled') }
-
- context 'when canceling' do
- before do
- click_on 'Cancel running'
- end
-
- it 'does not show a "Cancel running" button', :sidekiq_might_not_need_inline do
- expect(page).not_to have_content('Cancel running')
- end
- end
- end
-
- context 'playing manual job' do
- before do
- within '[data-testid="jobs-tab-table"]' do
- click_button('Play')
-
- wait_for_requests
- end
- end
-
- it { expect(build_manual.reload).to be_pending }
- end
-
- context 'when user unschedules a delayed job' do
- before do
- within '[data-testid="jobs-tab-table"]' do
- click_button('Unschedule')
- end
- end
-
- it 'unschedules the delayed job and shows play button as a manual job' do
- expect(page).to have_button('Play')
- expect(page).not_to have_button('Unschedule')
- end
- end
- end
-
- describe 'GET /:project/-/pipelines/:id/failures' do
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: '1234') }
- let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) }
- let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) }
-
- subject { visit pipeline_failures_page }
-
- context 'with failed build' do
- before do
- failed_build.trace.set('4 examples, 1 failure')
- end
-
- it 'lists failed builds' do
- subject
-
- expect(page).to have_content(failed_build.name)
- expect(page).to have_content(failed_build.stage_name)
- end
-
- it 'shows build failure logs' do
- subject
-
- expect(page).to have_content('4 examples, 1 failure')
- end
-
- it 'shows the failure reason' do
- subject
-
- expect(page).to have_content('There is an unknown failure, please try again')
- end
-
- context 'when user does not have permission to retry build' do
- it 'shows retry button for failed build' do
- subject
-
- page.within(find('#js-tab-failures', match: :first)) do
- expect(page).not_to have_button('Retry')
- end
- end
- end
-
- context 'when user does have permission to retry build' do
- before do
- create(:protected_branch, :developers_can_merge,
- name: pipeline.ref, project: project)
- end
-
- it 'shows retry button for failed build' do
- subject
-
- page.within(find('#js-tab-failures', match: :first)) do
- expect(page).to have_button('Retry')
- end
- end
- end
- end
-
- context 'when missing build logs' do
- it 'lists failed builds' do
- subject
-
- expect(page).to have_content(failed_build.name)
- expect(page).to have_content(failed_build.stage_name)
- end
-
- it 'does not show log' do
- subject
-
- expect(page).to have_content('No job log')
- end
- end
-
- context 'without permission to access builds' do
- let(:role) { :guest }
-
- before do
- project.update!(public_builds: false)
- end
-
- context 'when accessing failed jobs page' do
- it 'renders a 404 page' do
- requests = inspect_requests { subject }
-
- expect(page).to have_title('Not Found')
- expect(requests.first.status_code).to eq(404)
- end
- end
- end
-
- context 'without failures' do
- before do
- failed_build.update!(status: :success)
- end
-
- it 'does not show the failure tab' do
- subject
-
- expect(page).not_to have_content('Failed Jobs')
- end
-
- it 'displays the pipeline graph' do
- subject
-
- expect(page).to have_current_path(pipeline_path(pipeline), ignore_query: true)
- expect(page).to have_selector('.js-pipeline-graph')
- end
- end
- end
-end
diff --git a/spec/features/projects/pipelines/legacy_pipelines_spec.rb b/spec/features/projects/pipelines/legacy_pipelines_spec.rb
deleted file mode 100644
index 9d3ac71a875..00000000000
--- a/spec/features/projects/pipelines/legacy_pipelines_spec.rb
+++ /dev/null
@@ -1,852 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Pipelines', :js do
- include ProjectForksHelper
- include Spec::Support::Helpers::ModalHelpers
-
- let(:project) { create(:project) }
- let(:expected_detached_mr_tag) { 'merge request' }
-
- context 'when user is logged in' do
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
-
- project.add_developer(user)
- project.update!(auto_devops_attributes: { enabled: false })
-
- stub_feature_flags(pipeline_tabs_vue: false)
- end
-
- describe 'GET /:project/-/pipelines' do
- let(:project) { create(:project, :repository) }
-
- let!(:pipeline) do
- create(
- :ci_empty_pipeline,
- project: project,
- ref: 'master',
- status: 'running',
- sha: project.commit.id
- )
- end
-
- context 'scope' do
- before do
- create(:ci_empty_pipeline, status: 'pending', project: project, sha: project.commit.id, ref: 'master')
- create(:ci_empty_pipeline, status: 'running', project: project, sha: project.commit.id, ref: 'master')
- create(:ci_empty_pipeline, status: 'created', project: project, sha: project.commit.id, ref: 'master')
- create(:ci_empty_pipeline, status: 'success', project: project, sha: project.commit.id, ref: 'master')
- end
-
- [:all, :running, :pending, :finished, :branches].each do |scope|
- context "when displaying #{scope}" do
- before do
- visit_project_pipelines(scope: scope)
- end
-
- it 'contains pipeline commit short SHA' do
- expect(page).to have_content(pipeline.short_sha)
- end
-
- it 'contains branch name' do
- expect(page).to have_content(pipeline.ref)
- end
- end
- end
- end
-
- context 'header tabs' do
- before do
- visit project_pipelines_path(project)
- wait_for_requests
- end
-
- it 'shows a tab for All pipelines and count' do
- expect(page.find('.js-pipelines-tab-all').text).to include('All')
- expect(page.find('.js-pipelines-tab-all .badge').text).to include('1')
- end
-
- it 'shows a tab for Finished pipelines and count' do
- expect(page.find('.js-pipelines-tab-finished').text).to include('Finished')
- end
-
- it 'shows a tab for Branches' do
- expect(page.find('.js-pipelines-tab-branches').text).to include('Branches')
- end
-
- it 'shows a tab for Tags' do
- expect(page.find('.js-pipelines-tab-tags').text).to include('Tags')
- end
-
- it 'updates content when tab is clicked' do
- page.find('.js-pipelines-tab-finished').click
- wait_for_requests
- expect(page).to have_content('There are currently no finished pipelines.')
- end
- end
-
- context 'navigation links' do
- before do
- visit project_pipelines_path(project)
- wait_for_requests
- end
-
- it 'renders "CI lint" link' do
- expect(page).to have_link('CI lint')
- end
-
- it 'renders "Run pipeline" link' do
- expect(page).to have_link('Run pipeline')
- end
- end
-
- context 'when pipeline is cancelable' do
- let!(:build) do
- create(:ci_build, pipeline: pipeline,
- stage: 'test')
- end
-
- before do
- build.run
- visit_project_pipelines
- end
-
- it 'indicates that pipeline can be canceled' do
- expect(page).to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('.ci-running')
- end
-
- context 'when canceling' do
- before do
- find('.js-pipelines-cancel-button').click
- click_button 'Stop pipeline'
- wait_for_requests
- end
-
- 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('.ci-canceled')
- end
- end
- end
-
- context 'when pipeline is retryable', :sidekiq_might_not_need_inline do
- let!(:build) do
- create(:ci_build, pipeline: pipeline,
- stage: 'test')
- end
-
- before do
- build.drop
- visit_project_pipelines
- end
-
- it 'indicates that pipeline can be retried' do
- expect(page).to have_selector('.js-pipelines-retry-button')
- expect(page).to have_selector('.ci-failed')
- end
-
- context 'when retrying' do
- before do
- find('.js-pipelines-retry-button').click
- wait_for_requests
- end
-
- it 'shows running pipeline that is not retryable' do
- expect(page).not_to have_selector('.js-pipelines-retry-button')
- expect(page).to have_selector('.ci-running')
- end
- end
- end
-
- context 'when pipeline is detached merge request pipeline' do
- let(:merge_request) do
- create(:merge_request,
- :with_detached_merge_request_pipeline,
- source_project: source_project,
- target_project: target_project)
- end
-
- let!(:pipeline) { merge_request.all_pipelines.first }
- let(:source_project) { project }
- let(:target_project) { project }
-
- before do
- visit project_pipelines_path(source_project)
- end
-
- shared_examples_for 'detached merge request pipeline' do
- it 'shows pipeline information without pipeline ref', :sidekiq_might_not_need_inline 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).not_to have_link(pipeline.ref)
- end
- end
- end
-
- it_behaves_like 'detached merge request pipeline'
-
- context 'when source project is a forked project' do
- let(:source_project) { fork_project(project, user, repository: true) }
-
- it_behaves_like 'detached merge request pipeline'
- end
- end
-
- context 'when pipeline is merge request pipeline' do
- let(:merge_request) do
- create(:merge_request,
- :with_merge_request_pipeline,
- source_project: source_project,
- target_project: target_project,
- merge_sha: target_project.commit.sha)
- end
-
- let!(:pipeline) { merge_request.all_pipelines.first }
- let(:source_project) { project }
- let(:target_project) { project }
-
- before do
- visit project_pipelines_path(source_project)
- end
-
- shared_examples_for 'Correct merge request pipeline information' do
- it 'does not show detached tag for the pipeline, and shows the link of the merge request' \
- 'and does not show the ref of the pipeline', :sidekiq_might_not_need_inline 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).not_to have_link(pipeline.ref)
- end
- end
- end
-
- it_behaves_like 'Correct merge request pipeline information'
-
- context 'when source project is a forked project' do
- let(:source_project) { fork_project(project, user, repository: true) }
-
- it_behaves_like 'Correct merge request pipeline information'
- end
- end
-
- context 'when pipeline has configuration errors' do
- let(:pipeline) do
- create(:ci_pipeline, :invalid, project: project)
- end
-
- before do
- visit_project_pipelines
- end
-
- it 'contains badge that indicates errors' do
- expect(page).to have_content 'yaml invalid'
- end
-
- it 'contains badge with tooltip which contains error' do
- expect(pipeline).to have_yaml_errors
- expect(page).to have_selector(
- %Q{span[title="#{pipeline.yaml_errors}"]})
- end
-
- it 'contains badge that indicates failure reason' do
- expect(page).to have_content 'error'
- end
-
- it 'contains badge with tooltip which contains failure reason' do
- expect(pipeline.failure_reason?).to eq true
- expect(page).to have_selector(
- %Q{span[title="#{pipeline.present.failure_reason}"]})
- end
- end
-
- context 'with manual actions' do
- let!(:manual) do
- create(:ci_build, :manual,
- pipeline: pipeline,
- name: 'manual build',
- stage: 'test')
- end
-
- before do
- visit_project_pipelines
- end
-
- it 'has a dropdown with play button' do
- expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] [data-testid="play-icon"]')
- end
-
- it 'has link to the manual action' do
- find('[data-testid="pipelines-manual-actions-dropdown"]').click
-
- expect(page).to have_button('manual build')
- end
-
- context 'when manual action was played' do
- before do
- find('[data-testid="pipelines-manual-actions-dropdown"]').click
- click_button('manual build')
- end
-
- it 'enqueues manual action job' do
- expect(page).to have_selector(
- '[data-testid="pipelines-manual-actions-dropdown"] .gl-dropdown-toggle:disabled'
- )
- end
- end
- end
-
- context 'when there is a delayed job' do
- let!(:delayed_job) do
- create(:ci_build, :scheduled,
- pipeline: pipeline,
- name: 'delayed job 1',
- stage: 'test')
- end
-
- before do
- visit_project_pipelines
- end
-
- it 'has a dropdown for actionable jobs' do
- expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] [data-testid="play-icon"]')
- end
-
- it "has link to the delayed job's action" do
- find('[data-testid="pipelines-manual-actions-dropdown"]').click
-
- time_diff = [0, delayed_job.scheduled_at - Time.zone.now].max
- expect(page).to have_button('delayed job 1')
- expect(page).to have_content(Time.at(time_diff).utc.strftime("%H:%M:%S"))
- end
-
- context 'when delayed job is expired already' do
- let!(:delayed_job) do
- create(:ci_build, :expired_scheduled,
- pipeline: pipeline,
- name: 'delayed job 1',
- stage: 'test')
- end
-
- it "shows 00:00:00 as the remaining time" do
- find('[data-testid="pipelines-manual-actions-dropdown"]').click
-
- expect(page).to have_content("00:00:00")
- end
- end
-
- context 'when user played a delayed job immediately' do
- before do
- find('[data-testid="pipelines-manual-actions-dropdown"]').click
- accept_gl_confirm do
- click_button 'delayed job 1'
- end
- wait_for_requests
- end
-
- it 'enqueues the delayed job', :js do
- find('[data-testid="mini-pipeline-graph-dropdown"]').click
-
- within('[data-testid="mini-pipeline-graph-dropdown"]') { find('.ci-status-icon-pending') }
-
- expect(delayed_job.reload).to be_pending
- end
- end
- end
-
- context 'for generic statuses' do
- context 'when preparing' do
- let!(:pipeline) do
- create(:ci_empty_pipeline,
- status: 'preparing', project: project)
- end
-
- let!(:status) do
- create(:generic_commit_status,
- :preparing, pipeline: pipeline)
- end
-
- before do
- visit_project_pipelines
- end
-
- it 'is cancelable' do
- expect(page).to have_selector('.js-pipelines-cancel-button')
- end
-
- it 'shows the pipeline as preparing' do
- expect(page).to have_selector('.ci-preparing')
- end
- end
-
- context 'when running' do
- let!(:running) do
- create(:generic_commit_status,
- status: 'running',
- pipeline: pipeline,
- stage: 'test')
- end
-
- before do
- visit_project_pipelines
- end
-
- it 'is cancelable' do
- expect(page).to have_selector('.js-pipelines-cancel-button')
- end
-
- it 'has pipeline running' do
- expect(page).to have_selector('.ci-running')
- end
-
- context 'when canceling' do
- before do
- find('.js-pipelines-cancel-button').click
- click_button 'Stop pipeline'
- end
-
- 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('.ci-canceled')
- end
- end
- end
-
- context 'when failed' do
- let!(:status) do
- create(:generic_commit_status, :pending,
- pipeline: pipeline,
- stage: 'test')
- end
-
- before do
- status.drop
- visit_project_pipelines
- end
-
- it 'is not retryable' do
- expect(page).not_to have_selector('.js-pipelines-retry-button')
- end
-
- it 'has failed pipeline', :sidekiq_might_not_need_inline do
- expect(page).to have_selector('.ci-failed')
- end
- end
- end
-
- context 'downloadable pipelines' do
- context 'with artifacts' do
- let!(:with_artifacts) do
- build = create(:ci_build, :success,
- pipeline: pipeline,
- name: 'rspec tests',
- stage: 'test')
-
- create(:ci_job_artifact, :codequality, job: build)
- end
-
- before do
- visit_project_pipelines
- end
-
- it 'has artifacts dropdown' do
- expect(page).to have_selector('[data-testid="pipeline-multi-actions-dropdown"]')
- end
- end
-
- context 'with artifacts expired' do
- let!(:with_artifacts_expired) do
- create(:ci_build, :expired, :success,
- pipeline: pipeline,
- name: 'rspec',
- stage: 'test')
- end
-
- before do
- visit_project_pipelines
- end
-
- it { expect(page).not_to have_selector('[data-testid="artifact-item"]') }
- end
-
- context 'without artifacts' do
- let!(:without_artifacts) do
- create(:ci_build, :success,
- pipeline: pipeline,
- name: 'rspec',
- stage: 'test')
- end
-
- before do
- visit_project_pipelines
- end
-
- it { expect(page).not_to have_selector('[data-testid="artifact-item"]') }
- end
-
- context 'with trace artifact' do
- before do
- create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
-
- visit_project_pipelines
- end
-
- it 'does not show trace artifact as artifacts' do
- expect(page).not_to have_selector('[data-testid="artifact-item"]')
- end
- end
- end
-
- context 'mini pipeline graph' do
- let!(:build) do
- create(:ci_build, :pending, pipeline: pipeline,
- stage: 'build',
- name: 'build')
- end
-
- dropdown_selector = '[data-testid="mini-pipeline-graph-dropdown"]'
-
- before do
- visit_project_pipelines
- end
-
- it 'renders a mini pipeline graph' do
- expect(page).to have_selector('[data-testid="pipeline-mini-graph"]')
- expect(page).to have_selector(dropdown_selector)
- end
-
- context 'when clicking a stage badge' do
- it 'opens a dropdown' do
- find(dropdown_selector).click
-
- expect(page).to have_link build.name
- end
-
- it 'is possible to cancel pending build' do
- find(dropdown_selector).click
- find('.js-ci-action').click
- wait_for_requests
-
- expect(build.reload).to be_canceled
- end
- end
-
- context 'for a failed pipeline' do
- let!(:build) do
- create(:ci_build, :failed, pipeline: pipeline,
- stage: 'build',
- name: 'build')
- end
-
- it 'displays the failure reason' do
- find(dropdown_selector).click
-
- within('.js-builds-dropdown-list') do
- build_element = page.find('.mini-pipeline-graph-dropdown-item')
- expect(build_element['title']).to eq('build - failed - (unknown failure)')
- end
- end
- end
- end
-
- context 'with pagination' do
- before do
- allow(Ci::Pipeline).to receive(:default_per_page).and_return(1)
- create(:ci_empty_pipeline, project: project)
- end
-
- it 'renders pagination' do
- visit project_pipelines_path(project)
- wait_for_requests
-
- expect(page).to have_selector('.gl-pagination')
- end
-
- it 'renders second page of pipelines' do
- visit project_pipelines_path(project, page: '2')
- wait_for_requests
-
- expect(page).to have_selector('.gl-pagination .page-link', count: 4)
- end
-
- it 'shows updated content' do
- visit project_pipelines_path(project)
- wait_for_requests
- page.find('.page-link.next-page-item').click
-
- expect(page).to have_selector('.gl-pagination .page-link', count: 4)
- end
- end
-
- context 'with pipeline key selection' do
- before do
- visit project_pipelines_path(project)
- wait_for_requests
- end
-
- it 'changes the Pipeline ID column for Pipeline IID' do
- page.find('[data-testid="pipeline-key-dropdown"]').click
-
- within '.gl-new-dropdown-contents' do
- dropdown_options = page.find_all '.gl-new-dropdown-item'
-
- dropdown_options[1].click
- end
-
- expect(page.find('[data-testid="pipeline-th"]')).to have_content 'Pipeline'
- expect(page.find('[data-testid="pipeline-url-link"]')).to have_content "##{pipeline.iid}"
- end
- end
- end
-
- describe 'GET /:project/-/pipelines/show' do
- let(:project) { create(:project, :repository) }
-
- let(:pipeline) do
- create(:ci_empty_pipeline,
- project: project,
- sha: project.commit.id,
- user: user)
- end
-
- before do
- create_build('build', 0, 'build', :success)
- create_build('test', 1, 'rspec 0:2', :pending)
- create_build('test', 1, 'rspec 1:2', :running)
- create_build('test', 1, 'spinach 0:2', :created)
- create_build('test', 1, 'spinach 1:2', :created)
- create_build('test', 1, 'audit', :created)
- create_build('deploy', 2, 'production', :created)
-
- create(
- :generic_commit_status,
- pipeline: pipeline,
- stage: 'external',
- name: 'jenkins',
- stage_idx: 3,
- ref: 'master'
- )
-
- visit project_pipeline_path(project, pipeline)
- wait_for_requests
- end
-
- it 'shows a graph with grouped stages' do
- expect(page).to have_css('.js-pipeline-graph')
-
- # header
- expect(page).to have_text("##{pipeline.id}")
- expect(page).to have_selector(%Q(img[src="#{pipeline.user.avatar_url}"]))
- expect(page).to have_link(pipeline.user.name, href: user_path(pipeline.user))
-
- # stages
- expect(page).to have_text('build')
- expect(page).to have_text('test')
- expect(page).to have_text('deploy')
- expect(page).to have_text('external')
-
- # builds
- expect(page).to have_text('rspec')
- expect(page).to have_text('spinach')
- expect(page).to have_text('rspec')
- expect(page).to have_text('production')
- expect(page).to have_text('jenkins')
- end
-
- def create_build(stage, stage_idx, name, status)
- create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name, status: status)
- end
- end
-
- describe 'POST /:project/-/pipelines' do
- let(:project) { create(:project, :repository) }
-
- before do
- stub_feature_flags(run_pipeline_graphql: false)
- visit new_project_pipeline_path(project)
- end
-
- context 'for valid commit', :js do
- before do
- click_button project.default_branch
- wait_for_requests
-
- find('p', text: 'master').click
- wait_for_requests
- end
-
- context 'with gitlab-ci.yml', :js do
- before do
- stub_ci_pipeline_to_return_yaml_file
- end
-
- it 'creates a new pipeline' do
- expect do
- click_on 'Run pipeline'
- wait_for_requests
- end
- .to change { Ci::Pipeline.count }.by(1)
-
- expect(Ci::Pipeline.last).to be_web
- end
-
- context 'when variables are specified' do
- it 'creates a new pipeline with variables' do
- page.within(find("[data-testid='ci-variable-row']")) do
- find("[data-testid='pipeline-form-ci-variable-key']").set('key_name')
- find("[data-testid='pipeline-form-ci-variable-value']").set('value')
- end
-
- expect do
- click_on 'Run pipeline'
- wait_for_requests
- end
- .to change { Ci::Pipeline.count }.by(1)
-
- expect(Ci::Pipeline.last.variables.map { |var| var.slice(:key, :secret_value) })
- .to eq [{ key: "key_name", secret_value: "value" }.with_indifferent_access]
- end
- end
- end
-
- context 'without gitlab-ci.yml' do
- before do
- click_on 'Run pipeline'
- wait_for_requests
- end
-
- it { expect(page).to have_content('Missing CI config file') }
-
- it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file' \
- 'is available when trying again' do
- stub_ci_pipeline_to_return_yaml_file
-
- expect do
- click_on 'Run pipeline'
- wait_for_requests
- end
- .to change { Ci::Pipeline.count }.by(1)
- end
- end
- end
- end
-
- describe 'Reset runner caches' do
- let(:project) { create(:project, :repository) }
-
- before do
- create(:ci_empty_pipeline, status: 'success', project: project, sha: project.commit.id, ref: 'master')
- project.add_maintainer(user)
- visit project_pipelines_path(project)
- end
-
- it 'has a clear caches button' do
- expect(page).to have_button 'Clear runner caches'
- end
-
- describe 'user clicks the button' do
- context 'when project already has jobs_cache_index' do
- before do
- project.update!(jobs_cache_index: 1)
- end
-
- it 'increments jobs_cache_index' do
- click_button 'Clear runner caches'
- wait_for_requests
- expect(page.find('[data-testid="alert-info"]')).to have_content 'Project cache successfully reset.'
- end
- end
-
- context 'when project does not have jobs_cache_index' do
- it 'sets jobs_cache_index to 1' do
- click_button 'Clear runner caches'
- wait_for_requests
- expect(page.find('[data-testid="alert-info"]')).to have_content 'Project cache successfully reset.'
- end
- end
- end
- end
-
- describe 'Run Pipelines' do
- let(:project) { create(:project, :repository) }
-
- before do
- visit new_project_pipeline_path(project)
- end
-
- describe 'new pipeline page' do
- it 'has field to add a new pipeline' do
- expect(page).to have_selector('[data-testid="ref-select"]')
- expect(find('[data-testid="ref-select"]')).to have_content project.default_branch
- expect(page).to have_content('Run for')
- end
- end
-
- describe 'find pipelines' do
- it 'shows filtered pipelines', :js do
- click_button project.default_branch
-
- page.within '[data-testid="ref-select"]' do
- find('[data-testid="search-refs"]').native.send_keys('fix')
-
- page.within '.gl-new-dropdown-contents' do
- expect(page).to have_content('fix')
- end
- end
- end
- end
- end
-
- describe 'Empty State' do
- let(:project) { create(:project, :repository) }
-
- before do
- visit project_pipelines_path(project)
- end
-
- it 'renders empty state' do
- expect(page).to have_content 'Try test template'
- end
- end
- end
-
- context 'when user is not logged in' do
- before do
- project.update!(auto_devops_attributes: { enabled: false })
- visit project_pipelines_path(project)
- end
-
- context 'when project is public' do
- let(:project) { create(:project, :public, :repository) }
-
- context 'without pipelines' do
- it { expect(page).to have_content 'This project is not currently set up to run pipelines.' }
- end
- end
-
- context 'when project is private' do
- let(:project) { create(:project, :private, :repository) }
-
- it 'redirects the user to sign_in and displays the flash alert' do
- expect(page).to have_content 'You need to sign in'
- expect(page).to have_current_path("/users/sign_in")
- end
- end
- end
-
- def visit_project_pipelines(**query)
- visit project_pipelines_path(project, query)
- wait_for_requests
- end
-end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 2d729af513a..d6067e22952 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Pipeline', :js do
+RSpec.describe 'Pipeline', :js, feature_category: :projects do
include RoutesHelpers
include ProjectForksHelper
include ::ExclusiveLeaseHelpers
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index b7b715cb6db..3bdabd672c7 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Pipelines', :js do
+RSpec.describe 'Pipelines', :js, feature_category: :projects do
include ProjectForksHelper
include Spec::Support::Helpers::ModalHelpers
@@ -596,8 +596,8 @@ RSpec.describe 'Pipelines', :js do
it 'changes the Pipeline ID column for Pipeline IID' do
page.find('[data-testid="pipeline-key-dropdown"]').click
- within '.gl-new-dropdown-contents' do
- dropdown_options = page.find_all '.gl-new-dropdown-item'
+ within '.gl-dropdown-contents' do
+ dropdown_options = page.find_all '.gl-dropdown-item'
dropdown_options[1].click
end
@@ -663,7 +663,19 @@ RSpec.describe 'Pipelines', :js do
describe 'POST /:project/-/pipelines' do
let(:project) { create(:project, :repository) }
- shared_examples 'run pipeline form with gitlab-ci.yml' do
+ before do
+ visit new_project_pipeline_path(project)
+ end
+
+ context 'for valid commit', :js do
+ before do
+ click_button project.default_branch
+ wait_for_requests
+
+ find('p', text: 'master').click
+ wait_for_requests
+ end
+
context 'with gitlab-ci.yml', :js do
before do
stub_ci_pipeline_to_return_yaml_file
@@ -680,7 +692,7 @@ RSpec.describe 'Pipelines', :js do
end
context 'when variables are specified' do
- it 'creates a new pipeline with variables' do
+ it 'creates a new pipeline with variables', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375552' do
page.within(find("[data-testid='ci-variable-row']")) do
find("[data-testid='pipeline-form-ci-variable-key']").set('key_name')
find("[data-testid='pipeline-form-ci-variable-value']").set('value')
@@ -697,9 +709,7 @@ RSpec.describe 'Pipelines', :js do
end
end
end
- end
- shared_examples 'run pipeline form without gitlab-ci.yml' do
context 'without gitlab-ci.yml' do
before do
click_on 'Run pipeline'
@@ -708,7 +718,7 @@ RSpec.describe 'Pipelines', :js do
it { expect(page).to have_content('Missing CI config file') }
- it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again' do
+ it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375552' do
stub_ci_pipeline_to_return_yaml_file
expect do
@@ -719,52 +729,6 @@ RSpec.describe 'Pipelines', :js do
end
end
end
-
- # Run Pipeline form with REST endpoints
- # TODO: Clean up tests when run_pipeline_graphql is enabled
- # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/372310
- context 'with feature flag disabled' do
- before do
- stub_feature_flags(run_pipeline_graphql: false)
- visit new_project_pipeline_path(project)
- end
-
- context 'for valid commit', :js do
- before do
- click_button project.default_branch
- wait_for_requests
-
- find('p', text: 'master').click
- wait_for_requests
- end
-
- it_behaves_like 'run pipeline form with gitlab-ci.yml'
-
- it_behaves_like 'run pipeline form without gitlab-ci.yml'
- end
- end
-
- # Run Pipeline form with GraphQL
- context 'with feature flag enabled' do
- before do
- stub_feature_flags(run_pipeline_graphql: true)
- visit new_project_pipeline_path(project)
- end
-
- context 'for valid commit', :js do
- before do
- click_button project.default_branch
- wait_for_requests
-
- find('p', text: 'master').click
- wait_for_requests
- end
-
- it_behaves_like 'run pipeline form with gitlab-ci.yml'
-
- it_behaves_like 'run pipeline form without gitlab-ci.yml'
- end
- end
end
describe 'Reset runner caches' do
@@ -825,7 +789,7 @@ RSpec.describe 'Pipelines', :js do
page.within '[data-testid="ref-select"]' do
find('[data-testid="search-refs"]').native.send_keys('fix')
- page.within '.gl-new-dropdown-contents' do
+ page.within '.gl-dropdown-contents' do
expect(page).to have_content('fix')
end
end
diff --git a/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb b/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb
index 6745eb1a3fb..fb7814285b8 100644
--- a/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb
+++ b/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Raw > User interacts with raw endpoint' do
+RSpec.describe 'Projects > Raw > User interacts with raw endpoint', feature_category: :projects do
include RepoHelpers
let(:user) { create(:user) }
diff --git a/spec/features/projects/releases/user_creates_release_spec.rb b/spec/features/projects/releases/user_creates_release_spec.rb
index 4eb7581222e..f678d77b002 100644
--- a/spec/features/projects/releases/user_creates_release_spec.rb
+++ b/spec/features/projects/releases/user_creates_release_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User creates release', :js do
+RSpec.describe 'User creates release', :js, feature_category: :continuous_delivery do
include Spec::Support::Helpers::Features::ReleasesHelpers
let_it_be(:project) { create(:project, :repository) }
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 78b9798941a..ef3b35837ff 100644
--- a/spec/features/projects/releases/user_views_edit_release_spec.rb
+++ b/spec/features/projects/releases/user_views_edit_release_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User edits Release', :js do
+RSpec.describe 'User edits Release', :js, feature_category: :continuous_delivery do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/releases/user_views_release_spec.rb b/spec/features/projects/releases/user_views_release_spec.rb
index 4410f345e56..efa0ebd761d 100644
--- a/spec/features/projects/releases/user_views_release_spec.rb
+++ b/spec/features/projects/releases/user_views_release_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views Release', :js do
+RSpec.describe 'User views Release', :js, feature_category: :continuous_delivery do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/releases/user_views_releases_spec.rb b/spec/features/projects/releases/user_views_releases_spec.rb
index 10418e8072d..13dde57d885 100644
--- a/spec/features/projects/releases/user_views_releases_spec.rb
+++ b/spec/features/projects/releases/user_views_releases_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views releases', :js do
+RSpec.describe 'User views releases', :js, feature_category: :continuous_delivery do
let_it_be(:today) { Time.zone.now }
let_it_be(:yesterday) { today - 1.day }
let_it_be(:tomorrow) { today + 1.day }
diff --git a/spec/features/projects/remote_mirror_spec.rb b/spec/features/projects/remote_mirror_spec.rb
index 2c8e895d43d..aa0c1ead4c0 100644
--- a/spec/features/projects/remote_mirror_spec.rb
+++ b/spec/features/projects/remote_mirror_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project remote mirror', :feature do
+RSpec.describe 'Project remote mirror', :feature, feature_category: :projects do
let(:project) { create(:project, :repository, :remote_mirror) }
let(:remote_mirror) { project.remote_mirrors.first }
let(:user) { create(:user) }
diff --git a/spec/features/projects/settings/access_tokens_spec.rb b/spec/features/projects/settings/access_tokens_spec.rb
index 88f9a50b093..12e14f5193f 100644
--- a/spec/features/projects/settings/access_tokens_spec.rb
+++ b/spec/features/projects/settings/access_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project > Settings > Access Tokens', :js do
+RSpec.describe 'Project > Settings > Access Tokens', :js, feature_category: :credential_management do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/settings/branch_names_settings_spec.rb b/spec/features/projects/settings/branch_names_settings_spec.rb
index fdd883bc2b6..5d82dff1efd 100644
--- a/spec/features/projects/settings/branch_names_settings_spec.rb
+++ b/spec/features/projects/settings/branch_names_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project settings > repositories > Branch names', :js do
+RSpec.describe 'Project settings > repositories > Branch names', :js, feature_category: :projects do
let_it_be(:project) { create(:project, :public) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/settings/branch_rules_settings_spec.rb b/spec/features/projects/settings/branch_rules_settings_spec.rb
index 5cc35f108b5..71d9c559b77 100644
--- a/spec/features/projects/settings/branch_rules_settings_spec.rb
+++ b/spec/features/projects/settings/branch_rules_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > Repository > Branch rules settings' do
+RSpec.describe 'Projects > Settings > Repository > Branch rules settings', feature_category: :projects do
let(:project) { create(:project_empty_repo) }
let(:user) { create(:user) }
let(:role) { :developer }
diff --git a/spec/features/projects/settings/external_authorization_service_settings_spec.rb b/spec/features/projects/settings/external_authorization_service_settings_spec.rb
index c236c85b773..a99fd5f9788 100644
--- a/spec/features/projects/settings/external_authorization_service_settings_spec.rb
+++ b/spec/features/projects/settings/external_authorization_service_settings_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > External Authorization Classification Label setting' do
+RSpec.describe 'Projects > Settings > External Authorization Classification Label setting',
+feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project_empty_repo) }
diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb
index 04fb6953b51..28d5c080db9 100644
--- a/spec/features/projects/settings/forked_project_settings_spec.rb
+++ b/spec/features/projects/settings/forked_project_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > For a forked project', :js do
+RSpec.describe 'Projects > Settings > For a forked project', :js, feature_category: :projects do
include ProjectForksHelper
let(:user) { create(:user) }
let(:original_project) { create(:project) }
diff --git a/spec/features/projects/settings/lfs_settings_spec.rb b/spec/features/projects/settings/lfs_settings_spec.rb
index 6e1be3c7e51..1695b49830d 100644
--- a/spec/features/projects/settings/lfs_settings_spec.rb
+++ b/spec/features/projects/settings/lfs_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > LFS settings' do
+RSpec.describe 'Projects > Settings > LFS settings', feature_category: :projects do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:role) { :maintainer }
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index ba84d8b6d1a..ca90817b0a4 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > Merge requests' do
+RSpec.describe 'Projects > Settings > Merge requests', feature_category: :projects do
include ProjectForksHelper
let(:user) { create(:user) }
diff --git a/spec/features/projects/settings/monitor_settings_spec.rb b/spec/features/projects/settings/monitor_settings_spec.rb
index 871391fbe9c..2cdcf86757e 100644
--- a/spec/features/projects/settings/monitor_settings_spec.rb
+++ b/spec/features/projects/settings/monitor_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > For a forked project', :js do
+RSpec.describe 'Projects > Settings > For a forked project', :js, feature_category: :projects do
let_it_be(:project) { create(:project, :repository, create_templates: :issue) }
let(:user) { project.first_owner }
diff --git a/spec/features/projects/settings/packages_settings_spec.rb b/spec/features/projects/settings/packages_settings_spec.rb
index 1c2b0faa215..4ef17830f81 100644
--- a/spec/features/projects/settings/packages_settings_spec.rb
+++ b/spec/features/projects/settings/packages_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > Packages', :js do
+RSpec.describe 'Projects > Settings > Packages', :js, feature_category: :projects do
let_it_be(:project) { create(:project) }
let(:user) { project.first_owner }
@@ -33,6 +33,10 @@ RSpec.describe 'Projects > Settings > Packages', :js do
it 'displays the packages access level setting' do
expect(page).to have_selector('[data-testid="package-registry-access-level"] > label', text: 'Package registry')
+ expect(page).to have_selector('input[name="package_registry_enabled"]', visible: false)
+ expect(page).to have_selector('input[name="package_registry_enabled"] + button', visible: true)
+ expect(page).to have_selector('input[name="package_registry_api_for_everyone_enabled"]', visible: false)
+ expect(page).to have_selector('input[name="package_registry_api_for_everyone_enabled"] + button', visible: true)
expect(page).to have_selector(
'input[name="project[project_feature_attributes][package_registry_access_level]"]',
visible: false
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index a64f81430d1..37973c9b8d6 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Projects > Settings > Pipelines settings" do
+RSpec.describe "Projects > Settings > Pipelines settings", feature_category: :projects do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:role) { :developer }
@@ -165,7 +165,7 @@ RSpec.describe "Projects > Settings > Pipelines settings" do
let(:page_token) { find('#registration_token').text }
before do
- click_button 'Reset registration token'
+ click_link 'Reset registration token'
end
it 'changes registration token' do
diff --git a/spec/features/projects/settings/project_badges_spec.rb b/spec/features/projects/settings/project_badges_spec.rb
index 2c26168e3c0..f4c2265c2c2 100644
--- a/spec/features/projects/settings/project_badges_spec.rb
+++ b/spec/features/projects/settings/project_badges_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Badges' do
+RSpec.describe 'Project Badges', feature_category: :projects do
include WaitForRequests
let(:user) { create(:user) }
diff --git a/spec/features/projects/settings/project_settings_spec.rb b/spec/features/projects/settings/project_settings_spec.rb
index a0d44b579a8..46a41cfc6f1 100644
--- a/spec/features/projects/settings/project_settings_spec.rb
+++ b/spec/features/projects/settings/project_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects settings' do
+RSpec.describe 'Projects settings', feature_category: :projects do
let_it_be(:project) { create(:project) }
let(:user) { project.first_owner }
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 477c4c2e1ba..d4c1fe4d43e 100644
--- a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
+++ b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy' do
+RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy',
+feature_category: :projects do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index d64570cd5cc..072b5f7f3b0 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy' do
+RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy',
+feature_category: :projects do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index d73ff0284cd..6f0a3094849 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -2,12 +2,13 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > Repository settings' do
+RSpec.describe 'Projects > Settings > Repository settings', feature_category: :projects do
let(:project) { create(:project_empty_repo) }
let(:user) { create(:user) }
let(:role) { :developer }
before do
+ stub_feature_flags(branch_rules: false)
project.add_role(user, role)
sign_in(user)
end
@@ -39,19 +40,18 @@ RSpec.describe 'Projects > Settings > Repository settings' do
end
context 'Branch rules', :js do
- it 'renders branch rules settings' do
- visit project_settings_repository_path(project)
- expect(page).to have_content('Branch rules')
- end
-
context 'branch_rules feature flag disabled', :js do
it 'does not render branch rules settings' do
- stub_feature_flags(branch_rules: false)
visit project_settings_repository_path(project)
-
expect(page).not_to have_content('Branch rules')
end
end
+
+ it 'renders branch rules settings' do
+ stub_feature_flags(branch_rules: true)
+ visit project_settings_repository_path(project)
+ expect(page).to have_content('Branch rules')
+ end
end
context 'Deploy Keys', :js do
@@ -164,13 +164,7 @@ RSpec.describe 'Projects > Settings > Repository settings' do
end
project.reload
-
- # TODO: The following line is skipped because a toast with
- # "An error occurred while loading branch rules. Please try again."
- # shows up right after which hides the below message. It is causing flakiness.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/383717#note_1185091998
-
- # expect(page).to have_content('Mirroring settings were successfully updated')
+ expect(page).to have_content('Mirroring settings were successfully updated')
expect(project.remote_mirrors.first.only_protected_branches).to eq(false)
end
@@ -190,13 +184,7 @@ RSpec.describe 'Projects > Settings > Repository settings' do
end
project.reload
-
- # TODO: The following line is skipped because a toast with
- # "An error occurred while loading branch rules. Please try again."
- # shows up right after which hides the below message. It is causing flakiness.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/383717#note_1185091998
-
- # expect(page).to have_content('Mirroring settings were successfully updated')
+ expect(page).to have_content('Mirroring settings were successfully updated')
expect(project.remote_mirrors.first.only_protected_branches).to eq(true)
end
@@ -213,7 +201,12 @@ RSpec.describe 'Projects > Settings > Repository settings' do
click_button 'Mirror repository'
end
- expect(page).to have_content('Mirroring settings were successfully updated')
+ # TODO: The following line is skipped because a toast with
+ # "An error occurred while loading branch rules. Please try again."
+ # shows up right after which hides the below message. It is causing flakiness.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/383717#note_1185091998
+
+ # expect(page).to have_content('Mirroring settings were successfully updated')
expect(project.reload.remote_mirrors.first.keep_divergent_refs).to eq(true)
end
@@ -229,7 +222,12 @@ RSpec.describe 'Projects > Settings > Repository settings' do
click_button 'Mirror repository'
end
- expect(page).to have_content('Mirroring settings were successfully updated')
+ # TODO: The following line is skipped because a toast with
+ # "An error occurred while loading branch rules. Please try again."
+ # shows up right after which hides the below message. It is causing flakiness.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/383717#note_1185091998
+
+ # expect(page).to have_content('Mirroring settings were successfully updated')
expect(page).to have_selector('[title="Copy SSH public key"]')
end
@@ -272,7 +270,6 @@ RSpec.describe 'Projects > Settings > Repository settings' do
click_button 'Start cleanup'
end
end
-
expect(page).to have_content('Repository cleanup has started')
expect(RepositoryCleanupWorker.jobs.count).to eq(1)
end
diff --git a/spec/features/projects/settings/secure_files_spec.rb b/spec/features/projects/settings/secure_files_spec.rb
index ee38acf1953..9afe1f4de54 100644
--- a/spec/features/projects/settings/secure_files_spec.rb
+++ b/spec/features/projects/settings/secure_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Secure Files', :js do
+RSpec.describe 'Secure Files', :js, feature_category: :projects do
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -12,31 +12,10 @@ RSpec.describe 'Secure Files', :js do
sign_in(user)
end
- context 'when the :ci_secure_files feature flag is enabled' do
- before do
- stub_feature_flags(ci_secure_files: true)
-
- visit project_settings_ci_cd_path(project)
- end
-
- context 'authenticated user with admin permissions' do
- it 'shows the secure files settings' do
- expect(page).to have_content('Secure Files')
- end
- end
- end
-
- context 'when the :ci_secure_files feature flag is disabled' do
- before do
- stub_feature_flags(ci_secure_files: false)
-
+ context 'authenticated user with admin permissions' do
+ it 'shows the secure files settings' do
visit project_settings_ci_cd_path(project)
- end
-
- context 'authenticated user with admin permissions' do
- it 'does not shows the secure files settings' do
- expect(page).not_to have_content('Secure Files')
- end
+ expect(page).to have_content('Secure Files')
end
end
diff --git a/spec/features/projects/settings/service_desk_setting_spec.rb b/spec/features/projects/settings/service_desk_setting_spec.rb
index 86c5c3d2d8c..859c738731b 100644
--- a/spec/features/projects/settings/service_desk_setting_spec.rb
+++ b/spec/features/projects/settings/service_desk_setting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache do
+RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache, feature_category: :projects do
let(:project) { create(:project_empty_repo, :private, service_desk_enabled: false) }
let(:presenter) { project.present(current_user: user) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/settings/user_archives_project_spec.rb b/spec/features/projects/settings/user_archives_project_spec.rb
index 03ea9e7c580..a6aac02d272 100644
--- a/spec/features/projects/settings/user_archives_project_spec.rb
+++ b/spec/features/projects/settings/user_archives_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > User archives a project' do
+RSpec.describe 'Projects > Settings > User archives a project', feature_category: :projects do
let(:user) { create(:user) }
before do
diff --git a/spec/features/projects/settings/user_changes_avatar_spec.rb b/spec/features/projects/settings/user_changes_avatar_spec.rb
index 92d5b4c1fcd..87043aec9b6 100644
--- a/spec/features/projects/settings/user_changes_avatar_spec.rb
+++ b/spec/features/projects/settings/user_changes_avatar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > User changes avatar' do
+RSpec.describe 'Projects > Settings > User changes avatar', feature_category: :projects do
let(:project) { create(:project, :repository) }
let(:user) { project.creator }
diff --git a/spec/features/projects/settings/user_changes_default_branch_spec.rb b/spec/features/projects/settings/user_changes_default_branch_spec.rb
index bf064839bd7..39704fdbbb2 100644
--- a/spec/features/projects/settings/user_changes_default_branch_spec.rb
+++ b/spec/features/projects/settings/user_changes_default_branch_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > User changes default branch' do
+RSpec.describe 'Projects > Settings > User changes default branch', feature_category: :projects do
let(:user) { create(:user) }
before do
diff --git a/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
index 0fc12f93850..3a58de9aa7d 100644
--- a/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
+++ b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User interacts with deploy keys", :js do
+RSpec.describe "User interacts with deploy keys", :js, feature_category: :projects do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
index c76b4d0af88..cfefdd54c23 100644
--- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Projects > Settings > User manages merge request settings' do
+RSpec.describe 'Projects > Settings > User manages merge request settings', feature_category: :projects do
include ProjectForksHelper
let(:user) { create(:user) }
diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb
index 1d258582b3a..ee832da48d9 100644
--- a/spec/features/projects/settings/user_manages_project_members_spec.rb
+++ b/spec/features/projects/settings/user_manages_project_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > User manages project members' do
+RSpec.describe 'Projects > Settings > User manages project members', feature_category: :projects do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::ModalHelpers
@@ -51,8 +51,6 @@ RSpec.describe 'Projects > Settings > User manages project members' do
click_button 'Import project members'
wait_for_requests
- page.refresh
-
expect(find_member_row(user_mike)).to have_content('Reporter')
end
diff --git a/spec/features/projects/settings/user_renames_a_project_spec.rb b/spec/features/projects/settings/user_renames_a_project_spec.rb
index 2e2d7119e2e..2da6e760fbf 100644
--- a/spec/features/projects/settings/user_renames_a_project_spec.rb
+++ b/spec/features/projects/settings/user_renames_a_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > User renames a project' do
+RSpec.describe 'Projects > Settings > User renames a project', feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace, path: 'gitlab', name: 'sample') }
diff --git a/spec/features/projects/settings/user_searches_in_settings_spec.rb b/spec/features/projects/settings/user_searches_in_settings_spec.rb
index 7ed96d01189..8a11507d064 100644
--- a/spec/features/projects/settings/user_searches_in_settings_spec.rb
+++ b/spec/features/projects/settings/user_searches_in_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches project settings', :js do
+RSpec.describe 'User searches project settings', :js, feature_category: :projects do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: user.namespace, pages_https_only: false) }
diff --git a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
index 47383be1ba1..65aed4fd06f 100644
--- a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
+++ b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Repository Settings > User sees revoke deploy token modal', :js do
+RSpec.describe 'Repository Settings > User sees revoke deploy token modal', :js, feature_category: :projects do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:role) { :developer }
diff --git a/spec/features/projects/settings/user_tags_project_spec.rb b/spec/features/projects/settings/user_tags_project_spec.rb
index e9a2aa29352..43e8e5a2d38 100644
--- a/spec/features/projects/settings/user_tags_project_spec.rb
+++ b/spec/features/projects/settings/user_tags_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > User tags a project', :js do
+RSpec.describe 'Projects > Settings > User tags a project', :js, feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let!(:topic) { create(:topic, name: 'topic1') }
diff --git a/spec/features/projects/settings/user_transfers_a_project_spec.rb b/spec/features/projects/settings/user_transfers_a_project_spec.rb
index 23e10a36cee..53b4ee881f9 100644
--- a/spec/features/projects/settings/user_transfers_a_project_spec.rb
+++ b/spec/features/projects/settings/user_transfers_a_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > User transfers a project', :js do
+RSpec.describe 'Projects > Settings > User transfers a project', :js, feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:group) { create(:group) }
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
index 5cb12544066..5246eda976b 100644
--- a/spec/features/projects/settings/visibility_settings_spec.rb
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > Visibility settings', :js do
+RSpec.describe 'Projects > Settings > Visibility settings', :js, feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace, visibility_level: 20) }
@@ -28,26 +28,6 @@ RSpec.describe 'Projects > Settings > Visibility settings', :js do
expect(visibility_select_container).to have_content 'Only accessible by project members. Membership must be explicitly granted to each user.'
end
- context 'builds select' do
- it 'hides builds select section' do
- find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click
-
- visit project_settings_merge_requests_path(project)
-
- expect(page).to have_selector('.builds-feature', visible: false)
- end
-
- context 'given project with builds_disabled access level' do
- let(:project) { create(:project, :builds_disabled, namespace: user.namespace) }
-
- it 'hides builds select section' do
- visit project_settings_merge_requests_path(project)
-
- expect(page).to have_selector('.builds-feature', visible: false)
- end
- end
- end
-
context 'disable email notifications' do
it 'is visible' do
expect(page).to have_selector('.js-emails-disabled', visible: true)
diff --git a/spec/features/projects/settings/webhooks_settings_spec.rb b/spec/features/projects/settings/webhooks_settings_spec.rb
index adbf2f6ee5c..8d22d84b9c9 100644
--- a/spec/features/projects/settings/webhooks_settings_spec.rb
+++ b/spec/features/projects/settings/webhooks_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > Webhook Settings' do
+RSpec.describe 'Projects > Settings > Webhook Settings', feature_category: :projects do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:webhooks_path) { project_hooks_path(project) }
@@ -41,54 +41,28 @@ RSpec.describe 'Projects > Settings > Webhook Settings' do
expect(page).to have_content('Tag push events')
expect(page).to have_content('Issues events')
expect(page).to have_content('Confidential issues events')
- expect(page).to have_content('Note events')
- expect(page).to have_content('Merge requests events')
+ expect(page).to have_content('Comment')
+ expect(page).to have_content('Merge request events')
expect(page).to have_content('Pipeline events')
expect(page).to have_content('Wiki page events')
expect(page).to have_content('Releases events')
end
- context 'when feature flag "enhanced_webhook_support_regex" is disabled' do
- before do
- stub_feature_flags(enhanced_webhook_support_regex: false)
- end
-
- it 'create webhook', :js do
- visit webhooks_path
-
- fill_in 'URL', with: url
- check 'Tag push events'
- fill_in 'hook_push_events_branch_filter', with: 'master'
- check 'Enable SSL verification'
- check 'Job events'
-
- click_button 'Add webhook'
-
- expect(page).to have_content(url)
- expect(page).to have_content('SSL Verification: enabled')
- expect(page).to have_content('Tag push events')
- expect(page).to have_content('Job events')
- expect(page).to have_content('Push events')
- end
- end
-
- context 'when feature flag "enhanced_webhook_support_regex" is enabled' do
- it 'create webhook', :js do
- visit webhooks_path
+ it 'create webhook', :js do
+ visit webhooks_path
- fill_in 'URL', with: url
- check 'Tag push events'
- check 'Enable SSL verification'
- check 'Job events'
+ fill_in 'URL', with: url
+ check 'Tag push events'
+ check 'Enable SSL verification'
+ check 'Job events'
- click_button 'Add webhook'
+ click_button 'Add webhook'
- expect(page).to have_content(url)
- expect(page).to have_content('SSL Verification: enabled')
- expect(page).to have_content('Tag push events')
- expect(page).to have_content('Job events')
- expect(page).to have_content('Push events')
- end
+ expect(page).to have_content(url)
+ expect(page).to have_content('SSL Verification: enabled')
+ expect(page).to have_content('Tag push events')
+ expect(page).to have_content('Job events')
+ expect(page).to have_content('Push events')
end
it 'edit existing webhook', :js do
@@ -100,8 +74,8 @@ RSpec.describe 'Projects > Settings > Webhook Settings' do
check 'Enable SSL verification'
click_button 'Save changes'
- expect(page).to have_content 'SSL Verification: enabled'
- expect(page).to have_content(url)
+ expect(page).to have_content('Enable SSL verification')
+ expect(page).to have_current_path(edit_project_hook_path(project, hook), ignore_query: true)
end
it 'test existing webhook', :js do
diff --git a/spec/features/projects/show/download_buttons_spec.rb b/spec/features/projects/show/download_buttons_spec.rb
index e73bb3198e6..e4d50daa6f4 100644
--- a/spec/features/projects/show/download_buttons_spec.rb
+++ b/spec/features/projects/show/download_buttons_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > Download buttons' do
+RSpec.describe 'Projects > Show > Download buttons', feature_category: :projects do
let(:user) { create(:user) }
let(:role) { :developer }
let(:status) { 'success' }
diff --git a/spec/features/projects/show/no_password_spec.rb b/spec/features/projects/show/no_password_spec.rb
index ed06f4e14d3..9ead729af83 100644
--- a/spec/features/projects/show/no_password_spec.rb
+++ b/spec/features/projects/show/no_password_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'No Password Alert' do
+RSpec.describe 'No Password Alert', feature_category: :projects do
let_it_be(:message_password_auth_enabled) { 'Your account is authenticated with SSO or SAML. To push and pull over HTTP with Git using this account, you must set a password or set up a Personal Access Token to use instead of a password. For more information, see Clone with HTTPS.' }
let_it_be(:message_password_auth_disabled) { 'Your account is authenticated with SSO or SAML. To push and pull over HTTP with Git using this account, you must set up a Personal Access Token to use instead of a password. For more information, see Clone with HTTPS.' }
diff --git a/spec/features/projects/show/redirects_spec.rb b/spec/features/projects/show/redirects_spec.rb
index 55069cdd6c5..d1cb896450f 100644
--- a/spec/features/projects/show/redirects_spec.rb
+++ b/spec/features/projects/show/redirects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > Redirects' do
+RSpec.describe 'Projects > Show > Redirects', feature_category: :projects do
let(:user) { create :user }
let(:public_project) { create :project, :public }
let(:private_project) { create :project, :private }
diff --git a/spec/features/projects/show/rss_spec.rb b/spec/features/projects/show/rss_spec.rb
index 0bd6e9cbe3b..c2e8a844094 100644
--- a/spec/features/projects/show/rss_spec.rb
+++ b/spec/features/projects/show/rss_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > RSS' do
+RSpec.describe 'Projects > Show > RSS', feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:path) { project_path(project) }
diff --git a/spec/features/projects/show/schema_markup_spec.rb b/spec/features/projects/show/schema_markup_spec.rb
index 8adbdb64f1b..8262245c5cb 100644
--- a/spec/features/projects/show/schema_markup_spec.rb
+++ b/spec/features/projects/show/schema_markup_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > Schema Markup' do
+RSpec.describe 'Projects > Show > Schema Markup', feature_category: :projects do
let_it_be(:project) { create(:project, :repository, :public, :with_avatar, description: 'foobar', topic_list: 'topic1, topic2') }
it 'shows SoftwareSourceCode structured markup', :js do
diff --git a/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb b/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb
index 262885e09b3..2f33622d218 100644
--- a/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb
+++ b/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Project > Show > User interacts with auto devops implicitly enabled banner' do
+RSpec.describe 'Project > Show > User interacts with auto devops implicitly enabled banner',
+feature_category: :projects do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/show/user_interacts_with_stars_spec.rb b/spec/features/projects/show/user_interacts_with_stars_spec.rb
index 158b6aa9b46..e2166854ba3 100644
--- a/spec/features/projects/show/user_interacts_with_stars_spec.rb
+++ b/spec/features/projects/show/user_interacts_with_stars_spec.rb
@@ -2,13 +2,14 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > User interacts with project stars' do
+RSpec.describe 'Projects > Show > User interacts with project stars', feature_category: :projects do
let(:project) { create(:project, :public, :repository) }
context 'when user is signed in', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vscode_web_ide: false)
sign_in(user)
visit(project_path(project))
end
diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb
index 1df37eef6a9..8f6535fd4f0 100644
--- a/spec/features/projects/show/user_manages_notifications_spec.rb
+++ b/spec/features/projects/show/user_manages_notifications_spec.rb
@@ -2,10 +2,11 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > User manages notifications', :js do
+RSpec.describe 'Projects > Show > User manages notifications', :js, feature_category: :projects do
let(:project) { create(:project, :public, :repository) }
before do
+ stub_feature_flags(vscode_web_ide: false)
sign_in(project.first_owner)
end
@@ -23,7 +24,7 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do
click_notifications_button
page.within first('[data-testid="notification-dropdown"]') do
- expect(page.find('.gl-new-dropdown-item.is-active')).to have_content('On mention')
+ expect(page.find('.gl-dropdown-item.is-active')).to have_content('On mention')
expect(page).to have_css('[data-testid="notifications-icon"]')
end
end
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 c63427e56e6..145500a4c63 100644
--- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > Collaboration links', :js do
+RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :projects do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project, :repository, :public) }
diff --git a/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb b/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb
index 47e010dcf89..876eecfe559 100644
--- a/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb
+++ b/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > User sees a deletion failure message' do
+RSpec.describe 'Projects > Show > User sees a deletion failure message', feature_category: :projects do
let(:project) { create(:project, :empty_repo, pending_delete: true) }
before do
diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb
index 608bb4c5997..022f21f198d 100644
--- a/spec/features/projects/show/user_sees_git_instructions_spec.rb
+++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > User sees Git instructions' do
+RSpec.describe 'Projects > Show > User sees Git instructions', feature_category: :projects do
let_it_be(:user) { create(:user) }
before do
diff --git a/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb b/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb
index 0aa0f7754c6..25d241f004e 100644
--- a/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb
+++ b/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > User sees last commit CI status' do
+RSpec.describe 'Projects > Show > User sees last commit CI status', feature_category: :projects do
let_it_be(:project) { create(:project, :repository, :public) }
it 'shows the project README', :js do
diff --git a/spec/features/projects/show/user_sees_readme_spec.rb b/spec/features/projects/show/user_sees_readme_spec.rb
index 6a5b9472be8..a8c91b30f25 100644
--- a/spec/features/projects/show/user_sees_readme_spec.rb
+++ b/spec/features/projects/show/user_sees_readme_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > User sees README' do
+RSpec.describe 'Projects > Show > User sees README', feature_category: :projects do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :public) }
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index 5056e245fed..9eb2d109829 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do
+RSpec.describe 'Projects > Show > User sees setup shortcut buttons', feature_category: :projects do
# For "New file", "Add license" functionality,
# see spec/features/projects/files/project_owner_creates_license_file_spec.rb
# see spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
diff --git a/spec/features/projects/show/user_uploads_files_spec.rb b/spec/features/projects/show/user_uploads_files_spec.rb
index a222d6b42ab..ed378040ce9 100644
--- a/spec/features/projects/show/user_uploads_files_spec.rb
+++ b/spec/features/projects/show/user_uploads_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Show > User uploads files' do
+RSpec.describe 'Projects > Show > User uploads files', feature_category: :projects do
include DropzoneHelper
let(:user) { create(:user) }
diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb
index cbdf6d6852e..f2c575231ad 100644
--- a/spec/features/projects/snippets/create_snippet_spec.rb
+++ b/spec/features/projects/snippets/create_snippet_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Snippets > Create Snippet', :js do
+RSpec.describe 'Projects > Snippets > Create Snippet', :js, feature_category: :snippets do
include DropzoneHelper
include Spec::Support::Helpers::Features::SnippetSpecHelpers
diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb
index 5937ff75457..1a480696b4e 100644
--- a/spec/features/projects/snippets/show_spec.rb
+++ b/spec/features/projects/snippets/show_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Snippets > Project snippet', :js do
+RSpec.describe 'Projects > Snippets > Project snippet', :js, feature_category: :snippets do
let_it_be(:user) { create(:user) }
let_it_be(:project) do
create(:project, creator: user).tap do |p|
diff --git a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
index 3ccb73c88ef..556f549f86c 100644
--- a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
+++ b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Snippets > User comments on a snippet', :js do
+RSpec.describe 'Projects > Snippets > User comments on a snippet', :js, feature_category: :snippets do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:snippet) { create(:project_snippet, :repository, project: project, author: user) }
diff --git a/spec/features/projects/snippets/user_deletes_snippet_spec.rb b/spec/features/projects/snippets/user_deletes_snippet_spec.rb
index ca49e6a36b7..c9d1afb7a4e 100644
--- a/spec/features/projects/snippets/user_deletes_snippet_spec.rb
+++ b/spec/features/projects/snippets/user_deletes_snippet_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Snippets > User deletes a snippet', :js do
+RSpec.describe 'Projects > Snippets > User deletes a snippet', :js, feature_category: :snippets do
let(:project) { create(:project) }
let!(:snippet) { create(:project_snippet, :repository, project: project, author: user) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/snippets/user_updates_snippet_spec.rb b/spec/features/projects/snippets/user_updates_snippet_spec.rb
index aa498163f52..205db6c08b1 100644
--- a/spec/features/projects/snippets/user_updates_snippet_spec.rb
+++ b/spec/features/projects/snippets/user_updates_snippet_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Snippets > User updates a snippet', :js do
+RSpec.describe 'Projects > Snippets > User updates a snippet', :js, feature_category: :snippets do
include Spec::Support::Helpers::Features::SnippetSpecHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/snippets/user_views_snippets_spec.rb b/spec/features/projects/snippets/user_views_snippets_spec.rb
index 40539b43ed5..ece65763ea5 100644
--- a/spec/features/projects/snippets/user_views_snippets_spec.rb
+++ b/spec/features/projects/snippets/user_views_snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Snippets > User views snippets' do
+RSpec.describe 'Projects > Snippets > User views snippets', feature_category: :snippets do
let_it_be(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/sourcegraph_csp_spec.rb b/spec/features/projects/sourcegraph_csp_spec.rb
index 10dd050e8cc..4c8dd0a7df0 100644
--- a/spec/features/projects/sourcegraph_csp_spec.rb
+++ b/spec/features/projects/sourcegraph_csp_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Sourcegraph Content Security Policy' do
+RSpec.describe 'Sourcegraph Content Security Policy', feature_category: :projects do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: user.namespace) }
diff --git a/spec/features/projects/sub_group_issuables_spec.rb b/spec/features/projects/sub_group_issuables_spec.rb
index d7614201740..2502d969305 100644
--- a/spec/features/projects/sub_group_issuables_spec.rb
+++ b/spec/features/projects/sub_group_issuables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Subgroup Issuables', :js do
+RSpec.describe 'Subgroup Issuables', :js, feature_category: :projects do
let!(:group) { create(:group, name: 'group') }
let!(:subgroup) { create(:group, parent: group, name: 'subgroup') }
let!(:project) { create(:project, namespace: subgroup, name: 'project') }
diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb
index 0f1f72fd039..570721fc951 100644
--- a/spec/features/projects/tags/download_buttons_spec.rb
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Download buttons in tags page' do
+RSpec.describe 'Download buttons in tags page', feature_category: :source_code_management do
let(:user) { create(:user) }
let(:role) { :developer }
let(:status) { 'success' }
diff --git a/spec/features/projects/tags/user_edits_tags_spec.rb b/spec/features/projects/tags/user_edits_tags_spec.rb
index 857d0696659..e0efe3b465f 100644
--- a/spec/features/projects/tags/user_edits_tags_spec.rb
+++ b/spec/features/projects/tags/user_edits_tags_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project > Tags', :js do
+RSpec.describe 'Project > Tags', :js, feature_category: :source_code_management do
include DropzoneHelper
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/tags/user_views_tag_spec.rb b/spec/features/projects/tags/user_views_tag_spec.rb
index 3978c5b7b78..0816b3240c9 100644
--- a/spec/features/projects/tags/user_views_tag_spec.rb
+++ b/spec/features/projects/tags/user_views_tag_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'User views tag', :feature do
+RSpec.describe 'User views tag', :feature, feature_category: :source_code_management do
include_examples 'user views tag' do
let(:tag_page) { project_tag_path(project, id: tag_name) }
end
diff --git a/spec/features/projects/tags/user_views_tags_spec.rb b/spec/features/projects/tags/user_views_tags_spec.rb
index d3849df023e..26f2e81e3df 100644
--- a/spec/features/projects/tags/user_views_tags_spec.rb
+++ b/spec/features/projects/tags/user_views_tags_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'User views tags', :feature do
+RSpec.describe 'User views tags', :feature, feature_category: :source_code_management do
include_examples 'user views tag' do
let(:tag_page) { project_tags_path(project) }
end
diff --git a/spec/features/projects/terraform_spec.rb b/spec/features/projects/terraform_spec.rb
index d9e45b5e78e..bbc7f675c55 100644
--- a/spec/features/projects/terraform_spec.rb
+++ b/spec/features/projects/terraform_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Terraform', :js do
+RSpec.describe 'Terraform', :js, feature_category: :projects do
let_it_be(:project) { create(:project) }
let_it_be(:terraform_state) { create(:terraform_state, :locked, :with_version, project: project) }
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index 9c950cfee6e..3a0160c42fb 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Multi-file editor new directory', :js do
+RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_ide do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index c0567ed4580..61240150658 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Multi-file editor new file', :js do
+RSpec.describe 'Multi-file editor new file', :js, feature_category: :web_ide do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb
index efbfc329c9f..0b016ee3dd9 100644
--- a/spec/features/projects/tree/rss_spec.rb
+++ b/spec/features/projects/tree/rss_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Tree RSS' do
+RSpec.describe 'Project Tree RSS', feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:path) { project_tree_path(project, :master) }
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index eb0ef756b30..21932cae58b 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects tree', :js do
+RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
include RepoHelpers
let(:user) { create(:user) }
@@ -157,17 +157,22 @@ RSpec.describe 'Projects tree', :js do
end
end
- context 'ref switcher' do
+ context 'ref switcher', :js do
it 'switches ref to branch' do
+ ref_selector = '.ref-selector'
ref_name = 'feature'
visit project_tree_path(project, 'master')
- first('.js-project-refs-dropdown').click
- page.within '.project-refs-form' do
- click_link ref_name
+ find(ref_selector).click
+ wait_for_requests
+
+ page.within(ref_selector) do
+ fill_in 'Search by Git revision', with: ref_name
+ wait_for_requests
+ find('li', text: ref_name, match: :prefer_exact).click
end
- expect(page).to have_selector '.dropdown-menu-toggle', text: ref_name
+ expect(find(ref_selector)).to have_text(ref_name)
end
end
end
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
index f32141d6051..1e4abc789c2 100644
--- a/spec/features/projects/tree/upload_file_spec.rb
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Multi-file editor upload file', :js do
+RSpec.describe 'Multi-file editor upload file', :js, feature_category: :web_ide do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') }
diff --git a/spec/features/projects/user_changes_project_visibility_spec.rb b/spec/features/projects/user_changes_project_visibility_spec.rb
index df13bb55c6d..5daa5b98b6e 100644
--- a/spec/features/projects/user_changes_project_visibility_spec.rb
+++ b/spec/features/projects/user_changes_project_visibility_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User changes public project visibility', :js do
+RSpec.describe 'User changes public project visibility', :js, feature_category: :projects do
include ProjectForksHelper
shared_examples 'changing visibility to private' do
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index b07f2d12660..af0bd932095 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User creates a project', :js do
+RSpec.describe 'User creates a project', :js, feature_category: :projects do
let(:user) { create(:user) }
before do
diff --git a/spec/features/projects/user_sees_sidebar_spec.rb b/spec/features/projects/user_sees_sidebar_spec.rb
index e2498928fa0..3a6e11356a2 100644
--- a/spec/features/projects/user_sees_sidebar_spec.rb
+++ b/spec/features/projects/user_sees_sidebar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > User sees sidebar' do
+RSpec.describe 'Projects > User sees sidebar', feature_category: :projects do
let(:user) { create(:user) }
let(:project) { create(:project, :private, public_builds: false, namespace: user.namespace) }
@@ -220,7 +220,7 @@ RSpec.describe 'Projects > User sees sidebar' do
it 'does not show fork button' do
visit project_path(project)
- within('.count-buttons') do
+ within('.project-repo-buttons') do
expect(page).not_to have_link 'Fork'
end
end
diff --git a/spec/features/projects/user_sees_user_popover_spec.rb b/spec/features/projects/user_sees_user_popover_spec.rb
index 0bbe7f26cd4..5badcd99dff 100644
--- a/spec/features/projects/user_sees_user_popover_spec.rb
+++ b/spec/features/projects/user_sees_user_popover_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User sees user popover', :js do
+RSpec.describe 'User sees user popover', :js, feature_category: :projects do
include Spec::Support::Helpers::Features::NotesHelpers
let_it_be(:user) { create(:user, pronouns: 'they/them') }
diff --git a/spec/features/projects/user_sorts_projects_spec.rb b/spec/features/projects/user_sorts_projects_spec.rb
index c40f01f3aa1..6a18d95c840 100644
--- a/spec/features/projects/user_sorts_projects_spec.rb
+++ b/spec/features/projects/user_sorts_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User sorts projects and order persists' do
+RSpec.describe 'User sorts projects and order persists', feature_category: :projects do
include CookieHelper
let_it_be(:user) { create(:user) }
@@ -73,7 +73,7 @@ RSpec.describe 'User sorts projects and order persists' do
end
end
- it_behaves_like "sort order persists across all views", "Created date", "Created"
+ it_behaves_like "sort order persists across all views", "Oldest created", "Created"
end
context 'from group details', :js do
@@ -82,11 +82,11 @@ RSpec.describe 'User sorts projects and order persists' do
visit(details_group_path(group))
within '[data-testid=group_sort_by_dropdown]' do
find('button.gl-dropdown-toggle').click
- first(:button, 'Stars').click
+ first(:button, 'Updated').click
wait_for_requests
end
end
- it_behaves_like "sort order persists across all views", "Stars", "Stars"
+ it_behaves_like "sort order persists across all views", "Oldest updated", "Updated"
end
end
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
index cd6f09ce275..05d79ea3b1b 100644
--- a/spec/features/projects/user_uses_shortcuts_spec.rb
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User uses shortcuts', :js do
+RSpec.describe 'User uses shortcuts', :js, feature_category: :projects do
let_it_be(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
diff --git a/spec/features/projects/user_views_empty_project_spec.rb b/spec/features/projects/user_views_empty_project_spec.rb
index 696a7f4ee8a..352fa73bd05 100644
--- a/spec/features/projects/user_views_empty_project_spec.rb
+++ b/spec/features/projects/user_views_empty_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User views an empty project' do
+RSpec.describe 'User views an empty project', feature_category: :projects do
let_it_be(:project) { create(:project, :empty_repo) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index 5dd30f59e3d..bf32431fc88 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'View on environment', :js do
+RSpec.describe 'View on environment', :js, feature_category: :projects do
let(:branch_name) { 'feature' }
let(:file_path) { 'files/ruby/feature.rb' }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/wiki/user_views_wiki_empty_spec.rb b/spec/features/projects/wiki/user_views_wiki_empty_spec.rb
index ea045ddb6a1..1f3ba7a5ca2 100644
--- a/spec/features/projects/wiki/user_views_wiki_empty_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_empty_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project > User views empty wiki' do
+RSpec.describe 'Project > User views empty wiki', feature_category: :wiki do
let_it_be(:user) { create(:user) }
let(:wiki) { create(:project_wiki, project: project) }
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
index db2b3fc2f4b..79744633d0c 100644
--- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Wiki > User views wiki in project page' do
+RSpec.describe 'Projects > Wiki > User views wiki in project page', feature_category: :wiki do
before do
sign_in(project.first_owner)
end
diff --git a/spec/features/projects/wikis_spec.rb b/spec/features/projects/wikis_spec.rb
index 879ffd2932b..5d950da6674 100644
--- a/spec/features/projects/wikis_spec.rb
+++ b/spec/features/projects/wikis_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe 'Project wikis', :js do
+RSpec.describe 'Project wikis', :js, feature_category: :wiki do
let_it_be(:user) { create(:user) }
let(:wiki) { create(:project_wiki, user: user, project: project) }
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index cd9c173a4ff..ec0b3f9d81b 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project' do
+RSpec.describe 'Project', feature_category: :projects do
include ProjectForksHelper
include MobileHelpers
@@ -327,9 +327,9 @@ RSpec.describe 'Project' do
end
it 'has working links to submodules' do
- click_link('645f6c4c')
+ submodule = find_link('645f6c4c')
- expect(page).to have_selector('.ref-selector', text: '645f6c4c82fd3f5e06f67134450a570b795e55a6')
+ expect(submodule[:href]).to eq('https://gitlab.com/gitlab-org/gitlab-grack/-/tree/645f6c4c82fd3f5e06f67134450a570b795e55a6')
end
context 'for signed commit on default branch', :js do
@@ -418,7 +418,7 @@ RSpec.describe 'Project' do
visit path
end
- it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="project[name]"]' }]
+ it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="project[name]"]', submit: 'button[type="submit"]' }]
end
describe 'view for a user without an access to a repo' do
diff --git a/spec/features/promotion_spec.rb b/spec/features/promotion_spec.rb
index 903d6244a4c..b2ab718321f 100644
--- a/spec/features/promotion_spec.rb
+++ b/spec/features/promotion_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Promotions', :js do
+RSpec.describe 'Promotions', :js, feature_category: :service_desk do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project_empty_repo) }
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 174716d646d..c549d99a51f 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Protected Branches', :js do
+RSpec.describe 'Protected Branches', :js, feature_category: :source_code_management do
include ProtectedBranchHelpers
let(:user) { create(:user) }
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index c89dd4d1f90..1aadc7ce90a 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Protected Tags', :js do
+RSpec.describe 'Protected Tags', :js, feature_category: :source_code_management do
include ProtectedTagHelpers
let(:project) { create(:project, :repository) }
diff --git a/spec/features/read_only_spec.rb b/spec/features/read_only_spec.rb
index 11686552062..e65727c05e3 100644
--- a/spec/features/read_only_spec.rb
+++ b/spec/features/read_only_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'read-only message' do
+RSpec.describe 'read-only message', feature_category: :database do
let_it_be(:user) { create(:user) }
before do
@@ -14,7 +14,7 @@ RSpec.describe 'read-only message' do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
end
- it_behaves_like 'Read-only instance', /You are on a read\-only GitLab instance./
+ it_behaves_like 'Read-only instance', /You are on a read-only GitLab instance./
end
context 'when database is in read-write mode' do
@@ -22,6 +22,6 @@ RSpec.describe 'read-only message' do
allow(Gitlab::Database).to receive(:read_only?).and_return(false)
end
- it_behaves_like 'Read-write instance', /You are on a read\-only GitLab instance./
+ it_behaves_like 'Read-write instance', /You are on a read-only GitLab instance./
end
end
diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb
index 80c321d0f5a..55e7f5897bc 100644
--- a/spec/features/reportable_note/issue_spec.rb
+++ b/spec/features/reportable_note/issue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Reportable note on issue', :js do
+RSpec.describe 'Reportable note on issue', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/features/reportable_note/merge_request_spec.rb b/spec/features/reportable_note/merge_request_spec.rb
index 58a39bac707..39048495e5d 100644
--- a/spec/features/reportable_note/merge_request_spec.rb
+++ b/spec/features/reportable_note/merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Reportable note on merge request', :js do
+RSpec.describe 'Reportable note on merge request', :js, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/reportable_note/snippets_spec.rb b/spec/features/reportable_note/snippets_spec.rb
index 92bf304ac86..7e8c2c2f989 100644
--- a/spec/features/reportable_note/snippets_spec.rb
+++ b/spec/features/reportable_note/snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Reportable note on snippets', :js do
+RSpec.describe 'Reportable note on snippets', :js, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index cee0910aef7..40ba0fa9ebb 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Runners' do
+RSpec.describe 'Runners', feature_category: :runner_fleet do
let_it_be(:user) { create(:user) }
before do
@@ -117,11 +117,35 @@ RSpec.describe 'Runners' do
it 'user sees CI/CD setting page' do
visit project_runners_path(project)
- expect(page.find('.available-shared-runners')).to have_content(shared_runner.display_name)
+ within '[data-testid="available-shared-runners"]' do
+ expect(page).to have_content(shared_runner.display_name)
+ end
+ end
+
+ context 'when multiple shared runners are configured' do
+ let_it_be(:shared_runner_2) { create(:ci_runner, :instance) }
+
+ it 'shows the runner count' do
+ visit project_runners_path(project)
+
+ within '[data-testid="available-shared-runners"]' do
+ expect(page).to have_content format(_('Available shared runners: %{count}'), { count: 2 })
+ end
+ end
+
+ it 'adds pagination to the shared runner list' do
+ stub_const('Projects::Settings::CiCdController::NUMBER_OF_RUNNERS_PER_PAGE', 1)
+
+ visit project_runners_path(project)
+
+ within '[data-testid="available-shared-runners"]' do
+ expect(find('.pagination')).not_to be_nil
+ end
+ end
end
end
- context 'when multiple runners are configured' do
+ context 'when multiple project runners are configured' do
let!(:project_runner_2) { create(:ci_runner, :project, projects: [project]) }
it 'adds pagination to the runner list' do
@@ -306,7 +330,7 @@ RSpec.describe 'Runners' do
end
context 'project with a group and a group runner' do
- let_it_be(:ci_runner) do
+ let_it_be(:group_runner) do
create(:ci_runner, :group, groups: [group], description: 'group-runner')
end
@@ -330,6 +354,28 @@ RSpec.describe 'Runners' do
expect(page).to have_content 'Disable group runners'
expect(project.reload.group_runners_enabled).to be true
end
+
+ context 'when multiple group runners are configured' do
+ let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group]) }
+
+ it 'shows the runner count' do
+ visit project_runners_path(project)
+
+ within '[data-testid="group-runners"]' do
+ expect(page).to have_content format(_('Available group runners: %{runners}'), { runners: 2 })
+ end
+ end
+
+ it 'adds pagination to the group runner list' do
+ stub_const('Projects::Settings::CiCdController::NUMBER_OF_RUNNERS_PER_PAGE', 1)
+
+ visit project_runners_path(project)
+
+ within '[data-testid="group-runners"]' do
+ expect(find('.pagination')).not_to be_nil
+ end
+ end
+ end
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 ee74ac84a73..14d67bac85f 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches for code', :js, :disable_rate_limiter do
+RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_category: :global_search do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb
index 3c39e9f41d4..d7f6143d173 100644
--- a/spec/features/search/user_searches_for_comments_spec.rb
+++ b/spec/features/search/user_searches_for_comments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches for comments', :js, :disable_rate_limiter do
+RSpec.describe 'User searches for comments', :js, :disable_rate_limiter, feature_category: :global_search do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb
index e5d86c27942..1fd62a01c78 100644
--- a/spec/features/search/user_searches_for_commits_spec.rb
+++ b/spec/features/search/user_searches_for_commits_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches for commits', :js, :clean_gitlab_redis_rate_limiting do
+RSpec.describe 'User searches for commits', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index 22d48bd38f2..6ebbe86d1a9 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches for issues', :js, :clean_gitlab_redis_rate_limiting do
+RSpec.describe 'User searches for issues', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
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 9bbf2cf16d8..69f62a4c1e2 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches for merge requests', :js, :clean_gitlab_redis_rate_limiting do
+RSpec.describe 'User searches for merge requests', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
using RSpec::Parameterized::TableSyntax
let(:user) { create(:user) }
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index 702d4e60022..e87c2176380 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'User searches for milestones', :js, :clean_gitlab_redis_rate_limiting do
+RSpec.describe 'User searches for milestones', :js, :clean_gitlab_redis_rate_limiting,
+feature_category: :global_search do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb
index 15c6224b61b..48a94161927 100644
--- a/spec/features/search/user_searches_for_projects_spec.rb
+++ b/spec/features/search/user_searches_for_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches for projects', :js, :disable_rate_limiter do
+RSpec.describe 'User searches for projects', :js, :disable_rate_limiter, feature_category: :global_search do
let!(:project) { create(:project, :public, name: 'Shop') }
context 'when signed out' do
diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb
index 1d649b42c8c..4737cef98c7 100644
--- a/spec/features/search/user_searches_for_users_spec.rb
+++ b/spec/features/search/user_searches_for_users_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches for users', :js, :clean_gitlab_redis_rate_limiting do
+RSpec.describe 'User searches for users', :js, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
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') }
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 0f20ad0aa07..c7dc3e34bb7 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'User searches for wiki pages', :js, :clean_gitlab_redis_rate_limiting do
+RSpec.describe 'User searches for wiki pages', :js, :clean_gitlab_redis_rate_limiting,
+feature_category: :global_search do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
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 04f22cd2a31..334a192bec4 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User uses header search field', :js, :disable_rate_limiter do
+RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feature_category: :global_search do
include FilteredSearchHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb
index 24f6c70e64c..2e3aaab563d 100644
--- a/spec/features/search/user_uses_search_filters_spec.rb
+++ b/spec/features/search/user_uses_search_filters_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User uses search filters', :js do
+RSpec.describe 'User uses search filters', :js, feature_category: :global_search do
let(:group) { create(:group) }
let!(:group_project) { create(:project, group: group) }
let(:project) { create(:project, namespace: user.namespace) }
diff --git a/spec/features/security/admin_access_spec.rb b/spec/features/security/admin_access_spec.rb
index 8070ae066e7..de81444ed71 100644
--- a/spec/features/security/admin_access_spec.rb
+++ b/spec/features/security/admin_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Admin::Projects" do
+RSpec.describe "Admin::Projects", feature_category: :permissions do
include AccessMatchers
describe "GET /admin/projects" do
diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb
index 5430329d47d..948a4567624 100644
--- a/spec/features/security/dashboard_access_spec.rb
+++ b/spec/features/security/dashboard_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Dashboard access" do
+RSpec.describe "Dashboard access", feature_category: :permissions do
include AccessMatchers
describe "GET /dashboard" do
diff --git a/spec/features/security/group/internal_access_spec.rb b/spec/features/security/group/internal_access_spec.rb
index 755f170a93e..ad2df4a1882 100644
--- a/spec/features/security/group/internal_access_spec.rb
+++ b/spec/features/security/group/internal_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Internal Group access' do
+RSpec.describe 'Internal Group access', feature_category: :permissions do
include AccessMatchers
let(:group) { create(:group, :internal) }
@@ -27,9 +27,11 @@ RSpec.describe 'Internal Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -47,9 +49,11 @@ RSpec.describe 'Internal Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -69,9 +73,11 @@ RSpec.describe 'Internal Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -89,9 +95,11 @@ RSpec.describe 'Internal Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -109,9 +117,11 @@ RSpec.describe 'Internal Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_denied_for(:maintainer).of(group) }
it { is_expected.to be_denied_for(:developer).of(group) }
diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb
index f733145b5e3..2e7b7512b45 100644
--- a/spec/features/security/group/private_access_spec.rb
+++ b/spec/features/security/group/private_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Private Group access' do
+RSpec.describe 'Private Group access', feature_category: :permissions do
include AccessMatchers
let(:group) { create(:group, :private) }
@@ -27,9 +27,11 @@ RSpec.describe 'Private Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -47,9 +49,11 @@ RSpec.describe 'Private Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -69,9 +73,11 @@ RSpec.describe 'Private Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -89,9 +95,11 @@ RSpec.describe 'Private Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -109,9 +117,11 @@ RSpec.describe 'Private Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_denied_for(:maintainer).of(group) }
it { is_expected.to be_denied_for(:developer).of(group) }
@@ -135,9 +145,11 @@ RSpec.describe 'Private Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
diff --git a/spec/features/security/group/public_access_spec.rb b/spec/features/security/group/public_access_spec.rb
index 90de2b58044..513c5710c8f 100644
--- a/spec/features/security/group/public_access_spec.rb
+++ b/spec/features/security/group/public_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Public Group access' do
+RSpec.describe 'Public Group access', feature_category: :permissions do
include AccessMatchers
let(:group) { create(:group, :public) }
@@ -27,9 +27,11 @@ RSpec.describe 'Public Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -47,9 +49,11 @@ RSpec.describe 'Public Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -69,9 +73,11 @@ RSpec.describe 'Public Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -89,9 +95,11 @@ RSpec.describe 'Public Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_allowed_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
@@ -109,9 +117,11 @@ RSpec.describe 'Public Group access' do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed_for(:admin) }
end
+
context 'when admin mode is disabled' do
it { is_expected.to be_denied_for(:admin) }
end
+
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_denied_for(:maintainer).of(group) }
it { is_expected.to be_denied_for(:developer).of(group) }
diff --git a/spec/features/security/profile_access_spec.rb b/spec/features/security/profile_access_spec.rb
index 301efd2d99b..991ff115d3d 100644
--- a/spec/features/security/profile_access_spec.rb
+++ b/spec/features/security/profile_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Profile access" do
+RSpec.describe "Profile access", feature_category: :user_management do
include AccessMatchers
describe "GET /-/profile/keys" do
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 48cee4b1f19..e35e7ed742b 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Internal Project Access" do
+RSpec.describe "Internal Project Access", feature_category: :permissions do
include AccessMatchers
let_it_be(:project, reload: true) { create(:project, :internal, :repository, :with_namespace_settings) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index c06b1e5da54..59ddb18ae8a 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Private Project Access" do
+RSpec.describe "Private Project Access", feature_category: :permissions do
include AccessMatchers
let_it_be(:project, reload: true) do
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index d2112430638..425691001f2 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Public Project Access" do
+RSpec.describe "Public Project Access", feature_category: :permissions do
include AccessMatchers
let_it_be(:project, reload: true) do
diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb
index ab080f0a460..b7dcc5f31d3 100644
--- a/spec/features/security/project/snippet/internal_access_spec.rb
+++ b/spec/features/security/project/snippet/internal_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Internal Project Snippets Access" do
+RSpec.describe "Internal Project Snippets Access", feature_category: :permissions do
include AccessMatchers
let_it_be(:project) { create(:project, :internal) }
diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb
index 1e0afc09b74..0ae45abb7ec 100644
--- a/spec/features/security/project/snippet/private_access_spec.rb
+++ b/spec/features/security/project/snippet/private_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Private Project Snippets Access" do
+RSpec.describe "Private Project Snippets Access", feature_category: :permissions do
include AccessMatchers
let_it_be(:project) { create(:project, :private) }
diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb
index f734f7ba9e2..b98f665c0dc 100644
--- a/spec/features/security/project/snippet/public_access_spec.rb
+++ b/spec/features/security/project/snippet/public_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Public Project Snippets Access" do
+RSpec.describe "Public Project Snippets Access", feature_category: :permissions do
include AccessMatchers
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/features/sentry_js_spec.rb b/spec/features/sentry_js_spec.rb
index 1d277ba7b3c..d3880011914 100644
--- a/spec/features/sentry_js_spec.rb
+++ b/spec/features/sentry_js_spec.rb
@@ -2,27 +2,62 @@
require 'spec_helper'
-RSpec.describe 'Sentry' do
- let(:sentry_regex_path) { '\/sentry.*\.chunk\.js' }
+RSpec.describe 'Sentry', feature_category: :error_tracking do
+ context 'when enable_new_sentry_clientside_integration is disabled' do
+ before do
+ stub_feature_flags(enable_new_sentry_clientside_integration: false)
+ end
+
+ it 'does not load sentry if sentry is disabled' do
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(false)
+
+ visit new_user_session_path
+
+ expect(has_requested_legacy_sentry).to eq(false)
+ end
- it 'does not load sentry if sentry is disabled' do
- allow(Gitlab.config.sentry).to receive(:enabled).and_return(false)
- visit new_user_session_path
+ it 'loads legacy sentry if sentry config is enabled', :js do
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(true)
- expect(has_requested_sentry).to eq(false)
+ visit new_user_session_path
+
+ expect(has_requested_legacy_sentry).to eq(true)
+ expect(evaluate_script('window._Sentry.SDK_VERSION')).to match(%r{^5\.})
+ end
end
- it 'loads sentry if sentry is enabled' do
- stub_sentry_settings
+ context 'when enable_new_sentry_clientside_integration is enabled' do
+ before do
+ stub_feature_flags(enable_new_sentry_clientside_integration: true)
+ end
+
+ it 'does not load sentry if sentry settings are disabled' do
+ allow(Gitlab::CurrentSettings).to receive(:sentry_enabled).and_return(false)
- visit new_user_session_path
+ visit new_user_session_path
- expect(has_requested_sentry).to eq(true)
+ expect(has_requested_sentry).to eq(false)
+ end
+
+ it 'loads sentry if sentry settings are enabled', :js do
+ allow(Gitlab::CurrentSettings).to receive(:sentry_enabled).and_return(true)
+
+ visit new_user_session_path
+
+ expect(has_requested_sentry).to eq(true)
+ expect(evaluate_script('window._Sentry.SDK_VERSION')).to match(%r{^7\.})
+ end
+ end
+
+ def has_requested_legacy_sentry
+ page.all('script', visible: false).one? do |elm|
+ elm[:src] =~ %r{/legacy_sentry.*\.chunk\.js\z}
+ end
end
def has_requested_sentry
page.all('script', visible: false).one? do |elm|
- elm[:src] =~ /#{sentry_regex_path}$/
+ elm[:src] =~ %r{/sentry.*\.chunk\.js\z}
end
end
end
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index 8725dbcafe8..34127787e47 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'GPG signed commits' do
+RSpec.describe 'GPG signed commits', feature_category: :source_code_management do
let(:project) { create(:project, :public, :repository) }
it 'changes from unverified to verified when the user changes their email to match the gpg key', :sidekiq_might_not_need_inline do
diff --git a/spec/features/snippets/embedded_snippet_spec.rb b/spec/features/snippets/embedded_snippet_spec.rb
index 90d877d29b7..73b29ffd575 100644
--- a/spec/features/snippets/embedded_snippet_spec.rb
+++ b/spec/features/snippets/embedded_snippet_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Embedded Snippets' do
+RSpec.describe 'Embedded Snippets', feature_category: :source_code_management do
let_it_be(:snippet) { create(:personal_snippet, :public, :repository) }
let(:blobs) { snippet.blobs.first(3) }
diff --git a/spec/features/snippets/explore_spec.rb b/spec/features/snippets/explore_spec.rb
index b62c35bf96e..ef4b75ac3b4 100644
--- a/spec/features/snippets/explore_spec.rb
+++ b/spec/features/snippets/explore_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Explore Snippets' do
+RSpec.describe 'Explore Snippets', feature_category: :source_code_management do
let!(:public_snippet) { create(:personal_snippet, :public) }
let!(:internal_snippet) { create(:personal_snippet, :internal) }
let!(:private_snippet) { create(:personal_snippet, :private) }
diff --git a/spec/features/snippets/internal_snippet_spec.rb b/spec/features/snippets/internal_snippet_spec.rb
index 2fcd11c2a47..9645c9c110d 100644
--- a/spec/features/snippets/internal_snippet_spec.rb
+++ b/spec/features/snippets/internal_snippet_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Internal Snippets', :js do
+RSpec.describe 'Internal Snippets', :js, feature_category: :source_code_management do
let(:internal_snippet) { create(:personal_snippet, :internal, :repository) }
let(:content) { internal_snippet.blobs.first.data.strip! }
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
index 8d55a7a64f4..c281e5906ad 100644
--- a/spec/features/snippets/notes_on_personal_snippets_spec.rb
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Comments on personal snippets', :js do
+RSpec.describe 'Comments on personal snippets', :js, feature_category: :source_code_management do
include NoteInteractionHelpers
include Spec::Support::Helpers::ModalHelpers
diff --git a/spec/features/snippets/private_snippets_spec.rb b/spec/features/snippets/private_snippets_spec.rb
index 7ff27419cf7..0620a50ea72 100644
--- a/spec/features/snippets/private_snippets_spec.rb
+++ b/spec/features/snippets/private_snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Private Snippets', :js do
+RSpec.describe 'Private Snippets', :js, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:private_snippet) { create(:personal_snippet, :repository, :private, author: user) }
let(:content) { private_snippet.blobs.first.data.strip! }
diff --git a/spec/features/snippets/public_snippets_spec.rb b/spec/features/snippets/public_snippets_spec.rb
index 0f27d96d8e9..be6d6b2c0fa 100644
--- a/spec/features/snippets/public_snippets_spec.rb
+++ b/spec/features/snippets/public_snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Public Snippets', :js do
+RSpec.describe 'Public Snippets', :js, feature_category: :source_code_management do
let(:public_snippet) { create(:personal_snippet, :public, :repository) }
let(:content) { public_snippet.blobs.first.data.strip! }
diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb
index d18729d080a..98842f54015 100644
--- a/spec/features/snippets/search_snippets_spec.rb
+++ b/spec/features/snippets/search_snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Search Snippets', :js do
+RSpec.describe 'Search Snippets', :js, feature_category: :source_code_management do
it 'user searches for snippets by title' do
public_snippet = create(:personal_snippet, :public, title: 'Beginning and Middle')
private_snippet = create(:personal_snippet, :private, title: 'Middle and End')
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
index 2103d362f94..a6e0bc32d42 100644
--- a/spec/features/snippets/show_spec.rb
+++ b/spec/features/snippets/show_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Snippet', :js do
+RSpec.describe 'Snippet', :js, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:snippet) { create(:personal_snippet, :public, :repository, author: user) }
diff --git a/spec/features/snippets/spam_snippets_spec.rb b/spec/features/snippets/spam_snippets_spec.rb
index 3748a916780..5d49b36f4fe 100644
--- a/spec/features/snippets/spam_snippets_spec.rb
+++ b/spec/features/snippets/spam_snippets_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'snippet editor with spam', skip: "Will be handled in https://gitlab.com/gitlab-org/gitlab/-/issues/217722" do
+RSpec.describe 'snippet editor with spam', skip: "Will be handled in https://gitlab.com/gitlab-org/gitlab/-/issues/217722",
+ feature_category: :source_code_management do
include_context 'includes Spam constants'
let_it_be(:user) { create(:user) }
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index fd95516090a..064250c5673 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User creates snippet', :js do
+RSpec.describe 'User creates snippet', :js, feature_category: :source_code_management do
include DropzoneHelper
include Spec::Support::Helpers::Features::SnippetSpecHelpers
diff --git a/spec/features/snippets/user_deletes_snippet_spec.rb b/spec/features/snippets/user_deletes_snippet_spec.rb
index e896f7eb25b..3c4c41b0181 100644
--- a/spec/features/snippets/user_deletes_snippet_spec.rb
+++ b/spec/features/snippets/user_deletes_snippet_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User deletes snippet', :js do
+RSpec.describe 'User deletes snippet', :js, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:content) { 'puts "test"' }
let(:snippet) { create(:personal_snippet, :repository, :public, content: content, author: user) }
diff --git a/spec/features/snippets/user_edits_snippet_spec.rb b/spec/features/snippets/user_edits_snippet_spec.rb
index a04c59b53d2..5096472ebe1 100644
--- a/spec/features/snippets/user_edits_snippet_spec.rb
+++ b/spec/features/snippets/user_edits_snippet_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User edits snippet', :js do
+RSpec.describe 'User edits snippet', :js, feature_category: :source_code_management do
include DropzoneHelper
include Spec::Support::Helpers::Features::SnippetSpecHelpers
diff --git a/spec/features/snippets/user_snippets_spec.rb b/spec/features/snippets/user_snippets_spec.rb
index bb733431b22..09e0e30666d 100644
--- a/spec/features/snippets/user_snippets_spec.rb
+++ b/spec/features/snippets/user_snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User Snippets' do
+RSpec.describe 'User Snippets', feature_category: :source_code_management do
let(:author) { create(:user) }
let!(:public_snippet) { create(:personal_snippet, :public, author: author, title: "This is a public snippet") }
let!(:internal_snippet) { create(:personal_snippet, :internal, author: author, title: "This is an internal snippet") }
diff --git a/spec/features/snippets_spec.rb b/spec/features/snippets_spec.rb
index 35eb5c2e193..2ccdb68e844 100644
--- a/spec/features/snippets_spec.rb
+++ b/spec/features/snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Snippets' do
+RSpec.describe 'Snippets', feature_category: :snippets do
context 'when the project has snippets' do
let(:project) { create(:project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.first_owner, project: project) }
diff --git a/spec/features/tags/developer_creates_tag_spec.rb b/spec/features/tags/developer_creates_tag_spec.rb
index 5657115fb3c..111710ba325 100644
--- a/spec/features/tags/developer_creates_tag_spec.rb
+++ b/spec/features/tags/developer_creates_tag_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Developer creates tag' do
+RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: group) }
@@ -15,6 +15,8 @@ RSpec.describe 'Developer creates tag' do
context 'from tag list' do
before do
visit project_tags_path(project)
+ click_link 'New tag'
+ wait_for_requests
end
it 'with an invalid name displays an error' do
@@ -23,10 +25,17 @@ RSpec.describe 'Developer creates tag' do
expect(page).to have_content 'Tag name invalid'
end
- it 'with an invalid reference displays an error' do
- create_tag_in_form(tag: 'v2.0', ref: 'foo')
-
- expect(page).to have_content 'Target foo is invalid'
+ it "doesn't allow to select invalid ref" do
+ ref_name = 'foo'
+ fill_in 'tag_name', with: 'v2.0'
+ ref_selector = '.ref-selector'
+ find(ref_selector).click
+ wait_for_requests
+ page.within(ref_selector) do
+ fill_in _('Search by Git revision'), with: ref_name
+ wait_for_requests
+ expect(find('.gl-dropdown-contents')).not_to have_content(ref_name)
+ end
end
it 'that already exists displays an error' do
@@ -46,27 +55,34 @@ RSpec.describe 'Developer creates tag' do
end
end
- it 'opens dropdown for ref', :js do
- click_link 'New tag'
- ref_row = find('.form-group:nth-of-type(2) .col-sm-12')
+ it 'opens dropdown for ref' do
+ ref_row = find('.form-group:nth-of-type(2) .col-sm-auto')
page.within ref_row do
ref_input = find('[name="ref"]', visible: false)
expect(ref_input.value).to eq 'master'
- expect(find('.dropdown-toggle-text')).to have_content 'master'
-
- find('.js-branch-select').click
-
- expect(find('.dropdown-menu')).to have_content 'empty-branch'
+ expect(find('.gl-dropdown-button-text')).to have_content 'master'
+ find('.ref-selector').click
+ expect(find('.dropdown-menu')).to have_content 'test'
end
end
end
def create_tag_in_form(tag:, ref:, message: nil, desc: nil)
- click_link 'New tag'
fill_in 'tag_name', with: tag
- find('#ref', visible: false).set(ref)
+ select_ref(ref: ref)
fill_in 'message', with: message unless message.nil?
fill_in 'release_description', with: desc unless desc.nil?
click_button 'Create tag'
end
+
+ def select_ref(ref:)
+ ref_selector = '.ref-selector'
+ find(ref_selector).click
+ wait_for_requests
+ page.within(ref_selector) do
+ fill_in _('Search by Git revision'), with: ref
+ wait_for_requests
+ find('li', text: ref, match: :prefer_exact).click
+ end
+ end
end
diff --git a/spec/features/tags/developer_deletes_tag_spec.rb b/spec/features/tags/developer_deletes_tag_spec.rb
index efd4b42c136..76cf3aa691d 100644
--- a/spec/features/tags/developer_deletes_tag_spec.rb
+++ b/spec/features/tags/developer_deletes_tag_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Developer deletes tag', :js do
+RSpec.describe 'Developer deletes tag', :js, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: group) }
diff --git a/spec/features/tags/developer_views_tags_spec.rb b/spec/features/tags/developer_views_tags_spec.rb
index e2399dd9978..dc9f38f1d83 100644
--- a/spec/features/tags/developer_views_tags_spec.rb
+++ b/spec/features/tags/developer_views_tags_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Developer views tags' do
+RSpec.describe 'Developer views tags', feature_category: :source_code_management do
include RepoHelpers
let(:user) { create(:user) }
diff --git a/spec/features/tags/maintainer_deletes_protected_tag_spec.rb b/spec/features/tags/maintainer_deletes_protected_tag_spec.rb
index 0bf9645c2fb..ce518b962cd 100644
--- a/spec/features/tags/maintainer_deletes_protected_tag_spec.rb
+++ b/spec/features/tags/maintainer_deletes_protected_tag_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Maintainer deletes protected tag', :js do
+RSpec.describe 'Maintainer deletes protected tag', :js, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: group) }
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index 07de3789c08..d35726fe125 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Task Lists', :js do
+RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
include Warden::Test::Helpers
let_it_be(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/topic_show_spec.rb b/spec/features/topic_show_spec.rb
index 196fc34e3ea..d640e4e4edb 100644
--- a/spec/features/topic_show_spec.rb
+++ b/spec/features/topic_show_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Topic show page' do
+RSpec.describe 'Topic show page', feature_category: :projects do
let_it_be(:topic) { create(:topic, name: 'my-topic', title: 'My Topic', description: 'This is **my** topic https://google.com/ :poop: ```\ncode\n```', avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
context 'when topic does not exist' do
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index eb497715df7..3616fdb2e8e 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Triggers', :js do
+RSpec.describe 'Triggers', :js, feature_category: :continuous_integration do
include Spec::Support::Helpers::ModalHelpers
let(:trigger_title) { 'trigger desc' }
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index eed67e3ac78..9ef0626b2b2 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
+RSpec.describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js,
+feature_category: :authentication_and_authorization do
include Spec::Support::Helpers::Features::TwoFactorHelpers
before do
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index 12d2f0a9bb6..bcab35335cb 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Unsubscribe links', :sidekiq_inline do
+RSpec.describe 'Unsubscribe links', :sidekiq_inline, feature_category: :not_owned do
include Warden::Test::Helpers
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
index 8daa869a6e3..78cede77fea 100644
--- a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User uploads avatar to group' do
+RSpec.describe 'User uploads avatar to group', feature_category: :users do
it 'they see the new avatar' do
user = create(:user)
group = create(:group)
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 02f9d57fcfe..fb62b5eadc5 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User uploads avatar to profile' do
+RSpec.describe 'User uploads avatar to profile', feature_category: :users do
let!(:user) { create(:user) }
let(:avatar_file_path) { Rails.root.join('spec', 'fixtures', 'dk.png') }
diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb
index 2547e2d274c..e5ad62592ae 100644
--- a/spec/features/uploads/user_uploads_file_to_note_spec.rb
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User uploads file to note' do
+RSpec.describe 'User uploads file to note', feature_category: :team_planning do
include DropzoneHelper
let(:user) { create(:user) }
diff --git a/spec/features/usage_stats_consent_spec.rb b/spec/features/usage_stats_consent_spec.rb
index 69bd6f35558..c446fe1531b 100644
--- a/spec/features/usage_stats_consent_spec.rb
+++ b/spec/features/usage_stats_consent_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Usage stats consent' do
+RSpec.describe 'Usage stats consent', feature_category: :service_ping do
context 'when signed in' do
let(:user) { create(:admin, created_at: 8.days.ago) }
let(:message) { 'To help improve GitLab, we would like to periodically collect usage information.' }
diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb
index 14b5964686f..4f6ce6e8f71 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 do
+RSpec.describe 'User can display performance bar', :js, feature_category: :continuous_verification 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_opens_link_to_comment_spec.rb b/spec/features/user_opens_link_to_comment_spec.rb
index 59dea91c666..fb8f312c44b 100644
--- a/spec/features/user_opens_link_to_comment_spec.rb
+++ b/spec/features/user_opens_link_to_comment_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User opens link to comment', :js do
+RSpec.describe 'User opens link to comment', :js, feature_category: :team_planning do
let(:project) { create(:project, :public) }
let(:note) { create(:note_on_issue, project: project) }
diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb
index 5edf8358244..ea5fd537c5b 100644
--- a/spec/features/user_sees_revert_modal_spec.rb
+++ b/spec/features/user_sees_revert_modal_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not_need_inline do
+RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not_need_inline,
+feature_category: :code_review do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/user_sorts_things_spec.rb b/spec/features/user_sorts_things_spec.rb
index c6a1cfdc146..708caf79090 100644
--- a/spec/features/user_sorts_things_spec.rb
+++ b/spec/features/user_sorts_things_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe "User sorts things", :js do
sign_in(current_user)
end
- it "issues -> project home page -> issues" do
+ it "issues -> project home page -> issues", feature_category: :team_planning do
sort_option = s_('SortOptions|Updated date')
visit(project_issues_path(project))
@@ -34,7 +34,7 @@ RSpec.describe "User sorts things", :js do
expect(page).to have_button(sort_option)
end
- it "merge requests -> dashboard merge requests" do
+ it "merge requests -> dashboard merge requests", feature_category: :code_review do
sort_option = s_('SortOptions|Updated date')
visit(project_merge_requests_path(project))
diff --git a/spec/features/users/active_sessions_spec.rb b/spec/features/users/active_sessions_spec.rb
index e2ee78a7cc5..53a4c8a91e9 100644
--- a/spec/features/users/active_sessions_spec.rb
+++ b/spec/features/users/active_sessions_spec.rb
@@ -2,27 +2,31 @@
require 'spec_helper'
-RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions do
+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)
+
now = Time.zone.parse('2018-03-12 09:06')
- Timecop.freeze(now) do
- user = create(:user)
+ travel_to(now) do
gitlab_sign_in(user)
expect(page).to have_current_path root_path, ignore_query: true
sessions = ActiveSession.list(user)
expect(sessions.count).to eq 1
+ gitlab_sign_out
+ end
- # refresh the current page updates the updated_at
- Timecop.freeze(now + 1.minute) do
- visit current_path
+ # refresh the current page updates the updated_at
+ travel_to(now + 1.minute) do
+ gitlab_sign_in(user)
+
+ visit current_path
- 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')
- )
- end
+ 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')
+ )
end
end
diff --git a/spec/features/users/add_email_to_existing_account_spec.rb b/spec/features/users/add_email_to_existing_account_spec.rb
index cf78fc4587f..8c4e68c454f 100644
--- a/spec/features/users/add_email_to_existing_account_spec.rb
+++ b/spec/features/users/add_email_to_existing_account_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'AdditionalEmailToExistingAccount' do
+RSpec.describe 'AdditionalEmailToExistingAccount', feature_category: :users do
describe 'add secondary email associated with account' do
let_it_be(:user) { create(:user) }
let_it_be(:email) { create(:email, user: user) }
diff --git a/spec/features/users/anonymous_sessions_spec.rb b/spec/features/users/anonymous_sessions_spec.rb
index 6b21412ae3d..83473964d6b 100644
--- a/spec/features/users/anonymous_sessions_spec.rb
+++ b/spec/features/users/anonymous_sessions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Session TTLs', :clean_gitlab_redis_shared_state do
+RSpec.describe 'Session TTLs', :clean_gitlab_redis_shared_state, feature_category: :system_access do
include SessionHelpers
it 'creates a session with a short TTL when login fails' do
diff --git a/spec/features/users/bizible_csp_spec.rb b/spec/features/users/bizible_csp_spec.rb
index af0b42050b3..6c62cf9e0a2 100644
--- a/spec/features/users/bizible_csp_spec.rb
+++ b/spec/features/users/bizible_csp_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Bizible content security policy' do
+RSpec.describe 'Bizible content security policy', feature_category: :purchase do
before do
stub_config(extra: { one_trust_id: SecureRandom.uuid })
end
diff --git a/spec/features/users/confirmation_spec.rb b/spec/features/users/confirmation_spec.rb
index aaa49c75223..cf8d0c4dbd4 100644
--- a/spec/features/users/confirmation_spec.rb
+++ b/spec/features/users/confirmation_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User confirmation' do
+RSpec.describe 'User confirmation', feature_category: :system_access do
describe 'resend confirmation instructions' do
context 'when recaptcha is enabled' do
before do
diff --git a/spec/features/users/email_verification_on_login_spec.rb b/spec/features/users/email_verification_on_login_spec.rb
index f7102eaf9b7..de52f0b517e 100644
--- a/spec/features/users/email_verification_on_login_spec.rb
+++ b/spec/features/users/email_verification_on_login_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting do
+RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting, feature_category: :system_access do
include EmailHelpers
let_it_be(:user) { create(:user) }
@@ -223,6 +223,14 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting
it_behaves_like 'email verification required'
it_behaves_like 'no email verification required when 2fa enabled or ff disabled'
+
+ context 'when the check_ip_address_for_email_verification feature flag is disabled' do
+ before do
+ stub_feature_flags(check_ip_address_for_email_verification: false)
+ end
+
+ it_behaves_like 'no email verification required'
+ end
end
describe 'when a previous authentication event exists for the same ip address' do
diff --git a/spec/features/users/google_analytics_csp_spec.rb b/spec/features/users/google_analytics_csp_spec.rb
index 46a9b3be22f..45cc6c5f39d 100644
--- a/spec/features/users/google_analytics_csp_spec.rb
+++ b/spec/features/users/google_analytics_csp_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Google Analytics 4 content security policy' do
+RSpec.describe 'Google Analytics 4 content security policy', feature_category: :purchase do
it 'includes the GA4 content security policy headers' do
visit root_path
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 5ca5bd72b79..105e9f97989 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Login', :clean_gitlab_redis_sessions do
+RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_access do
include TermsHelper
include UserLoginHelper
include SessionHelpers
@@ -103,7 +103,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
let(:alert_message) { "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select Resend confirmation email" }
before do
- stub_application_setting(send_user_confirmation_email: true)
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
stub_feature_flags(identity_verification: false)
end
@@ -953,7 +953,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions do
let(:alert_message) { "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select Resend confirmation email" }
before do
- stub_application_setting(send_user_confirmation_email: true)
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
stub_feature_flags(soft_email_confirmation: true)
stub_feature_flags(identity_verification: false)
allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
diff --git a/spec/features/users/logout_spec.rb b/spec/features/users/logout_spec.rb
index 596f0dd5a94..c9839247e7d 100644
--- a/spec/features/users/logout_spec.rb
+++ b/spec/features/users/logout_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Logout/Sign out', :js do
+RSpec.describe 'Logout/Sign out', :js, feature_category: :system_access do
let(:user) { create(:user) }
before do
diff --git a/spec/features/users/one_trust_csp_spec.rb b/spec/features/users/one_trust_csp_spec.rb
index 382a0b4be6c..c22fd26f2e8 100644
--- a/spec/features/users/one_trust_csp_spec.rb
+++ b/spec/features/users/one_trust_csp_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OneTrust content security policy' do
+RSpec.describe 'OneTrust content security policy', feature_category: :application_instrumentation do
let(:user) { create(:user) }
before do
diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb
index 902079b7b93..489e7d61ff9 100644
--- a/spec/features/users/overview_spec.rb
+++ b/spec/features/users/overview_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Overview tab on a user profile', :js do
+RSpec.describe 'Overview tab on a user profile', :js, feature_category: :users do
let(:user) { create(:user) }
let(:contributed_project) { create(:project, :public, :repository) }
diff --git a/spec/features/users/password_spec.rb b/spec/features/users/password_spec.rb
index 793a11c616e..ccd383c8a15 100644
--- a/spec/features/users/password_spec.rb
+++ b/spec/features/users/password_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User password' do
+RSpec.describe 'User password', feature_category: :system_access do
describe 'send password reset' do
context 'when recaptcha is enabled' do
before do
diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb
index aba1ff63fab..a2604cd298a 100644
--- a/spec/features/users/rss_spec.rb
+++ b/spec/features/users/rss_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User RSS' do
+RSpec.describe 'User RSS', feature_category: :users do
let(:user) { create(:user) }
let(:path) { user_path(create(:user)) }
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index bbf5882f89f..318dd688fa4 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User page' do
+RSpec.describe 'User page', feature_category: :users do
include ExternalAuthorizationServiceHelpers
let_it_be(:user) { create(:user, bio: '<b>Lorem</b> <i>ipsum</i> dolor sit <a href="https://example.com">amet</a>') }
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 9b1a102f07b..1057ae48c7d 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -44,7 +44,7 @@ RSpec.shared_examples 'Signup name validation' do |field, max_length, label|
end
end
-RSpec.describe 'Signup' do
+RSpec.describe 'Signup', feature_category: :users do
include TermsHelper
let(:new_user) { build_stubbed(:user) }
@@ -197,7 +197,7 @@ RSpec.describe 'Signup' do
context 'with no errors' do
context 'when sending confirmation email' do
before do
- stub_application_setting(send_user_confirmation_email: true)
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
end
context 'when soft email confirmation is not enabled' do
@@ -239,7 +239,7 @@ RSpec.describe 'Signup' do
context "when not sending confirmation email" do
before do
- stub_application_setting(send_user_confirmation_email: false)
+ stub_application_setting_enum('email_confirmation_setting', 'off')
end
it 'creates the user account and goes to dashboard' do
@@ -282,7 +282,7 @@ RSpec.describe 'Signup' do
expect(page).to have_content("Email has already been taken")
end
- it 'does not redisplay the password' do
+ it 'redisplays all fields except password' do
create(:user, email: new_user.email)
visit new_user_registration_path
@@ -291,6 +291,11 @@ RSpec.describe 'Signup' do
expect(page).to have_current_path user_registration_path, ignore_query: true
expect(page.body).not_to match(/#{new_user.password}/)
+
+ expect(find_field('First name').value).to eq(new_user.first_name)
+ expect(find_field('Last name').value).to eq(new_user.last_name)
+ expect(find_field('Username').value).to eq(new_user.username)
+ expect(find_field('Email').value).to eq(new_user.email)
end
end
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index ce19e491a7c..20fc2981418 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Snippets tab on a user profile', :js do
+RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :snippets do
context 'when the user has snippets' do
let(:user) { create(:user) }
diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb
index 7a662d24d60..7d2137b81b8 100644
--- a/spec/features/users/terms_spec.rb
+++ b/spec/features/users/terms_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Users > Terms', :js do
+RSpec.describe 'Users > Terms', :js, feature_category: :users do
include TermsHelper
let!(:term) { create(:term, terms: 'By accepting, you promise to be nice!') }
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 5e7d7b76843..841b324fba4 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Users > User browses projects on user page', :js do
+RSpec.describe 'Users > User browses projects on user page', :js, feature_category: :projects do
let!(:user) { create :user }
let!(:private_project) do
create :project, :private, name: 'private', namespace: user.namespace do |project|
diff --git a/spec/features/users/zuora_csp_spec.rb b/spec/features/users/zuora_csp_spec.rb
index f3fd27d6495..b07c923fa54 100644
--- a/spec/features/users/zuora_csp_spec.rb
+++ b/spec/features/users/zuora_csp_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Zuora content security policy' do
+RSpec.describe 'Zuora content security policy', feature_category: :purchase do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/features/webauthn_spec.rb b/spec/features/webauthn_spec.rb
index 215d1ff1cb6..e2f16f4a017 100644
--- a/spec/features/webauthn_spec.rb
+++ b/spec/features/webauthn_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Using WebAuthn Devices for Authentication', :js do
+RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_category: :authentication_and_authorization do
include Spec::Support::Helpers::Features::TwoFactorHelpers
let(:app_id) { "http://#{Capybara.current_session.server.host}:#{Capybara.current_session.server.port}" }
diff --git a/spec/features/whats_new_spec.rb b/spec/features/whats_new_spec.rb
index 2938ea1b1e8..6b19ab28b44 100644
--- a/spec/features/whats_new_spec.rb
+++ b/spec/features/whats_new_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "renders a `whats new` dropdown item" do
+RSpec.describe "renders a `whats new` dropdown item", feature_category: :not_owned do
let_it_be(:user) { create(:user) }
context 'when not logged in' do
diff --git a/spec/features/work_items/work_item_children_spec.rb b/spec/features/work_items/work_item_children_spec.rb
index 10a1bf7541e..4403ca60d11 100644
--- a/spec/features/work_items/work_item_children_spec.rb
+++ b/spec/features/work_items/work_item_children_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Work item children', :js do
+RSpec.describe 'Work item children', :js, feature_category: :team_planning do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :public, namespace: group) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/work_items/work_item_spec.rb b/spec/features/work_items/work_item_spec.rb
index 686b82de868..577ec060020 100644
--- a/spec/features/work_items/work_item_spec.rb
+++ b/spec/features/work_items/work_item_spec.rb
@@ -2,14 +2,16 @@
require 'spec_helper'
-RSpec.describe 'Work item', :js do
+RSpec.describe 'Work item', :js, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { create(:user) }
+ let_it_be(:other_user) { create(:user) }
let_it_be(:work_item) { create(:work_item, project: project) }
context 'for signed in user' do
before do
project.add_developer(user)
+ project.add_developer(other_user)
sign_in(user)
@@ -28,6 +30,31 @@ RSpec.describe 'Work item', :js do
expect(page).to have_text(user.name)
end
end
+
+ it 'shows conflict message when description changes', :aggregate_failures do
+ click_button "Edit description"
+ scroll_to(find('[aria-label="Description"]'))
+
+ # without this for some reason the test fails when running locally
+ sleep 1
+
+ ::WorkItems::UpdateService.new(
+ project: work_item.project,
+ current_user: other_user,
+ params: { description: "oh no!" }
+ ).execute(work_item)
+
+ work_item.reload
+
+ find('[aria-label="Description"]').send_keys("oh yeah!")
+
+ warning = 'Someone edited the description at the same time you did.'
+ expect(page.find('[data-testid="work-item-description-conflicts"]')).to have_text(warning)
+
+ click_button "Save and overwrite"
+
+ expect(page.find('[data-testid="work-item-description"]')).to have_text("oh yeah!")
+ end
end
end
end
diff --git a/spec/finders/autocomplete/routes_finder_spec.rb b/spec/finders/autocomplete/routes_finder_spec.rb
index c5b040a5640..f37e8e8de7b 100644
--- a/spec/finders/autocomplete/routes_finder_spec.rb
+++ b/spec/finders/autocomplete/routes_finder_spec.rb
@@ -32,8 +32,24 @@ RSpec.describe Autocomplete::RoutesFinder do
context 'when user is admin' do
let(:current_user) { admin }
- it 'finds all namespaces matching the search excluding project namespaces' do
- is_expected.to match_array([group.route, group2.route, user_route])
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it 'finds all namespaces matching the search excluding project namespaces' do
+ is_expected.to match_array([group.route, group2.route, user_route])
+ end
+ end
+
+ context 'when admin mode setting is enabled' do
+ context 'when in admin mode', :enable_admin_mode do
+ it 'finds all namespaces matching the search excluding project namespaces' do
+ is_expected.to match_array([group.route, group2.route, user_route])
+ end
+ end
+
+ context 'when not in admin mode' do
+ it 'does not find all namespaces' do
+ is_expected.to match_array([])
+ end
+ end
end
end
end
@@ -48,8 +64,24 @@ RSpec.describe Autocomplete::RoutesFinder do
context 'when user is admin' do
let(:current_user) { admin }
- it 'finds all projects matching the search' do
- is_expected.to match_array([project.route, project2.route])
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it 'finds all projects matching the search' do
+ is_expected.to match_array([project.route, project2.route])
+ end
+ end
+
+ context 'when admin mode setting is enabled' do
+ context 'when in admin mode', :enable_admin_mode do
+ it 'finds all projects matching the search' do
+ is_expected.to match_array([project.route, project2.route])
+ end
+ end
+
+ context 'when not in admin mode' do
+ it 'does not find all projects' do
+ is_expected.to match_array([])
+ end
+ end
end
end
end
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
index f14c60c4b8f..18f8d1adecc 100644
--- a/spec/finders/branches_finder_spec.rb
+++ b/spec/finders/branches_finder_spec.rb
@@ -72,16 +72,6 @@ RSpec.describe BranchesFinder do
end
end
- context 'with an unknown name' do
- let(:params) { { search: 'random' } }
-
- it 'does not find any branch' do
- result = subject
-
- expect(result.count).to eq(0)
- end
- end
-
context 'by provided names' do
let(:params) { { names: %w[fix csv lfs does-not-exist] } }
@@ -115,6 +105,49 @@ RSpec.describe BranchesFinder do
end
end
+ context 'by name with wildcard' do
+ let(:params) { { search: 'f*e' } }
+
+ it 'filters branches' do
+ result = subject
+
+ expect(result.first.name).to eq('2-mb-file')
+ expect(result.count).to eq(30)
+ end
+ end
+
+ context 'by mixed regex operators' do
+ let(:params) { { search: '^f*e$' } }
+
+ it 'filters branches' do
+ result = subject
+
+ expect(result.first.name).to eq('feature')
+ expect(result.count).to eq(1)
+ end
+ end
+
+ context 'by name with multiple wildcards' do
+ let(:params) { { search: 'f*a*e' } }
+
+ it 'filters branches' do
+ result = subject
+
+ expect(result.first.name).to eq('after-create-delete-modify-move')
+ expect(result.count).to eq(11)
+ end
+ end
+
+ context 'with an unknown name' do
+ let(:params) { { search: 'random' } }
+
+ it 'does not find any branch' do
+ result = subject
+
+ expect(result.count).to eq(0)
+ end
+ end
+
context 'by nonexistent name that begins with' do
let(:params) { { search: '^nope' } }
@@ -134,6 +167,16 @@ RSpec.describe BranchesFinder do
expect(result.count).to eq(0)
end
end
+
+ context 'by nonexistent name with wildcard' do
+ let(:params) { { search: 'zz*asdf' } }
+
+ it 'filters branches' do
+ result = subject
+
+ expect(result.count).to eq(0)
+ end
+ end
end
context 'filter and sort' do
diff --git a/spec/finders/ci/auth_job_finder_spec.rb b/spec/finders/ci/auth_job_finder_spec.rb
index 0a326699875..73a65d0c5af 100644
--- a/spec/finders/ci/auth_job_finder_spec.rb
+++ b/spec/finders/ci/auth_job_finder_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::AuthJobFinder do
+RSpec.describe Ci::AuthJobFinder, feature_category: :continuous_integration do
let_it_be(:user, reload: true) { create(:user) }
let_it_be(:job, reload: true) { create(:ci_build, status: :running, user: user) }
@@ -68,7 +68,7 @@ RSpec.describe Ci::AuthJobFinder do
it 'sets ci_job_token_scope on the job user', :aggregate_failures do
expect(subject).to eq(job)
expect(subject.user).to be_from_ci_job_token
- expect(subject.user.ci_job_token_scope.source_project).to eq(job.project)
+ expect(subject.user.ci_job_token_scope.current_project).to eq(job.project)
end
end
end
diff --git a/spec/finders/freeze_periods_finder_spec.rb b/spec/finders/ci/freeze_periods_finder_spec.rb
index 53cc07d91b0..6c58028a221 100644
--- a/spec/finders/freeze_periods_finder_spec.rb
+++ b/spec/finders/ci/freeze_periods_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe FreezePeriodsFinder do
+RSpec.describe Ci::FreezePeriodsFinder, feature_category: :release_orchestration do
subject(:finder) { described_class.new(project, user).execute }
let(:project) { create(:project, :private) }
diff --git a/spec/finders/ci/jobs_finder_spec.rb b/spec/finders/ci/jobs_finder_spec.rb
index dd3ba9721e4..0b3777a2fe8 100644
--- a/spec/finders/ci/jobs_finder_spec.rb
+++ b/spec/finders/ci/jobs_finder_spec.rb
@@ -14,52 +14,55 @@ RSpec.describe Ci::JobsFinder, '#execute' do
let(:params) { {} }
context 'no project' do
- subject { described_class.new(current_user: admin, params: params).execute }
+ subject { described_class.new(current_user: current_user, params: params).execute }
- it 'returns all jobs' do
- expect(subject).to match_array([pending_job, running_job, successful_job])
- end
+ context 'with admin' do
+ let(:current_user) { admin }
- context 'non admin user' do
- let(:admin) { user }
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it { is_expected.to match_array([pending_job, running_job, successful_job]) }
+ end
- it 'returns no jobs' do
- expect(subject).to be_empty
+ context 'when admin mode setting is enabled' do
+ context 'when in admin mode', :enable_admin_mode do
+ it { is_expected.to match_array([pending_job, running_job, successful_job]) }
+ end
+
+ context 'when not in admin mode' do
+ it { is_expected.to be_empty }
+ end
end
end
+ context 'with normal user' do
+ let(:current_user) { user }
+
+ it { is_expected.to be_empty }
+ end
+
context 'without user' do
- let(:admin) { nil }
+ let(:current_user) { nil }
- it 'returns no jobs' do
- expect(subject).to be_empty
- end
+ it { is_expected.to be_empty }
end
- context 'scope is present' do
+ context 'with scope', :enable_admin_mode do
+ let(:current_user) { admin }
let(:jobs) { [pending_job, running_job, successful_job] }
- where(:scope, :index) do
- [
- ['pending', 0],
- ['running', 1],
- ['finished', 2]
- ]
+ using RSpec::Parameterized::TableSyntax
+
+ where(:scope, :expected_jobs) do
+ 'pending' | lazy { [pending_job] }
+ 'running' | lazy { [running_job] }
+ 'finished' | lazy { [successful_job] }
+ %w[running success] | lazy { [running_job, successful_job] }
end
with_them do
let(:params) { { scope: scope } }
- it { expect(subject).to match_array([jobs[index]]) }
- end
- end
-
- context 'scope is an array' do
- let(:jobs) { [pending_job, running_job, successful_job, canceled_job] }
- let(:params) { { scope: %w'running success' } }
-
- it 'filters by the job statuses in the scope' do
- expect(subject).to contain_exactly(running_job, successful_job)
+ it { is_expected.to match_array(expected_jobs) }
end
end
end
@@ -96,6 +99,33 @@ RSpec.describe Ci::JobsFinder, '#execute' do
end
end
+ context 'when artifacts are present for some jobs' do
+ let_it_be(:job_with_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'test') }
+ let_it_be(:artifact) { create(:ci_job_artifact, job: job_with_artifacts) }
+
+ subject { described_class.new(current_user: user, project: project, params: params).execute }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when with_artifacts is true' do
+ let(:params) { { with_artifacts: true } }
+
+ it 'returns only jobs with artifacts' do
+ expect(subject).to match_array([job_with_artifacts])
+ end
+ end
+
+ context 'when with_artifacts is false' do
+ let(:params) { { with_artifacts: false } }
+
+ it 'returns all jobs' do
+ expect(subject).to match_array([successful_job, job_with_artifacts])
+ end
+ end
+ end
+
context 'when pipeline is present' do
before_all do
project.add_maintainer(user)
diff --git a/spec/finders/ci/pipelines_finder_spec.rb b/spec/finders/ci/pipelines_finder_spec.rb
index 908210e0296..a2e8fe8df5a 100644
--- a/spec/finders/ci/pipelines_finder_spec.rb
+++ b/spec/finders/ci/pipelines_finder_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::PipelinesFinder do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
let(:params) { {} }
@@ -242,6 +242,45 @@ RSpec.describe Ci::PipelinesFinder do
end
end
+ context 'when name is specified' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, name: 'Build pipeline') }
+ let_it_be(:pipeline_other) { create(:ci_pipeline, project: project, name: 'Some other pipeline') }
+
+ let(:params) { { name: 'build Pipeline' } }
+
+ it 'performs case insensitive compare' do
+ is_expected.to contain_exactly(pipeline)
+ end
+
+ context 'when name does not exist' do
+ let(:params) { { name: 'absent-name' } }
+
+ it 'returns empty' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when pipeline_name feature flag is off' do
+ before do
+ stub_feature_flags(pipeline_name: false)
+ end
+
+ it 'ignores name parameter' do
+ is_expected.to contain_exactly(pipeline, pipeline_other)
+ end
+ end
+
+ context 'when pipeline_name_search feature flag is off' do
+ before do
+ stub_feature_flags(pipeline_name_search: false)
+ end
+
+ it 'ignores name parameter' do
+ is_expected.to contain_exactly(pipeline, pipeline_other)
+ end
+ end
+ end
+
describe 'ordering' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb
index 18eecd0f073..a8ef99eeaec 100644
--- a/spec/finders/ci/runners_finder_spec.rb
+++ b/spec/finders/ci/runners_finder_spec.rb
@@ -7,219 +7,221 @@ RSpec.describe Ci::RunnersFinder do
let_it_be(:admin) { create(:user, :admin) }
describe '#execute' do
- context 'with 2 runners' do
- let_it_be(:runner1) { create(:ci_runner, active: true) }
- let_it_be(:runner2) { create(:ci_runner, active: false) }
-
- context 'with empty params' do
- it 'returns all runners' do
- expect(Ci::Runner).to receive(:with_tags).and_call_original
- expect(described_class.new(current_user: admin, params: {}).execute).to match_array [runner1, runner2]
+ shared_examples 'executes as admin' do
+ context 'with 2 runners' do
+ let_it_be(:runner1) { create(:ci_runner, active: true) }
+ let_it_be(:runner2) { create(:ci_runner, active: false) }
+
+ context 'with empty params' do
+ it 'returns all runners' do
+ expect(Ci::Runner).to receive(:with_tags).and_call_original
+ expect(described_class.new(current_user: admin, params: {}).execute).to match_array [runner1, runner2]
+ end
end
- end
- context 'with nil group' do
- it 'returns all runners' do
- expect(Ci::Runner).to receive(:with_tags).and_call_original
- expect(described_class.new(current_user: admin, params: { group: nil }).execute).to match_array [runner1, runner2]
+ context 'with nil group' do
+ it 'returns all runners' do
+ expect(Ci::Runner).to receive(:with_tags).and_call_original
+ expect(described_class.new(current_user: admin, params: { group: nil }).execute).to match_array [runner1, runner2]
+ end
end
- end
- context 'with preload param set to :tag_name true' do
- it 'requests tags' do
- expect(Ci::Runner).to receive(:with_tags).and_call_original
- expect(described_class.new(current_user: admin, params: { preload: { tag_name: true } }).execute).to match_array [runner1, runner2]
+ context 'with preload param set to :tag_name true' do
+ it 'requests tags' do
+ expect(Ci::Runner).to receive(:with_tags).and_call_original
+ expect(described_class.new(current_user: admin, params: { preload: { tag_name: true } }).execute).to match_array [runner1, runner2]
+ end
end
- end
- context 'with preload param set to :tag_name false' do
- it 'does not request tags' do
- expect(Ci::Runner).not_to receive(:with_tags)
- expect(described_class.new(current_user: admin, params: { preload: { tag_name: false } }).execute).to match_array [runner1, runner2]
+ context 'with preload param set to :tag_name false' do
+ it 'does not request tags' do
+ expect(Ci::Runner).not_to receive(:with_tags)
+ expect(described_class.new(current_user: admin, params: { preload: { tag_name: false } }).execute).to match_array [runner1, runner2]
+ end
end
end
- end
- context 'filtering' do
- context 'by search term' do
- it 'calls Ci::Runner.search' do
- expect(Ci::Runner).to receive(:search).with('term').and_call_original
+ context 'filtering' do
+ context 'by search term' do
+ it 'calls Ci::Runner.search' do
+ expect(Ci::Runner).to receive(:search).with('term').and_call_original
- described_class.new(current_user: admin, params: { search: 'term' }).execute
+ described_class.new(current_user: admin, params: { search: 'term' }).execute
+ end
end
- end
- context 'by upgrade status' do
- let(:upgrade_status) {}
+ context 'by upgrade status' do
+ let(:upgrade_status) {}
- let_it_be(:runner1) { create(:ci_runner, version: 'a') }
- let_it_be(:runner2) { create(:ci_runner, version: 'b') }
- let_it_be(:runner3) { create(:ci_runner, version: 'c') }
- let_it_be(:runner_version_recommended) do
- create(:ci_runner_version, version: 'a', status: :recommended)
- end
+ let_it_be(:runner1) { create(:ci_runner, version: 'a') }
+ let_it_be(:runner2) { create(:ci_runner, version: 'b') }
+ let_it_be(:runner3) { create(:ci_runner, version: 'c') }
+ let_it_be(:runner_version_recommended) do
+ create(:ci_runner_version, version: 'a', status: :recommended)
+ end
- let_it_be(:runner_version_not_available) do
- create(:ci_runner_version, version: 'b', status: :not_available)
- end
+ let_it_be(:runner_version_not_available) do
+ create(:ci_runner_version, version: 'b', status: :not_available)
+ end
- let_it_be(:runner_version_available) do
- create(:ci_runner_version, version: 'c', status: :available)
- end
+ let_it_be(:runner_version_available) do
+ create(:ci_runner_version, version: 'c', status: :available)
+ end
- def execute
- described_class.new(current_user: admin, params: { upgrade_status: upgrade_status }).execute
- end
+ def execute
+ described_class.new(current_user: admin, params: { upgrade_status: upgrade_status }).execute
+ end
- Ci::RunnerVersion.statuses.keys.map(&:to_sym).each do |status|
- context "set to :#{status}" do
- let(:upgrade_status) { status }
+ Ci::RunnerVersion.statuses.keys.map(&:to_sym).each do |status|
+ context "set to :#{status}" do
+ let(:upgrade_status) { status }
- it "calls with_upgrade_status scope with corresponding :#{status} status" do
- if [:available, :not_available, :recommended].include?(status)
- expected_result = Ci::Runner.with_upgrade_status(status)
- end
+ it "calls with_upgrade_status scope with corresponding :#{status} status" do
+ if [:available, :not_available, :recommended].include?(status)
+ expected_result = Ci::Runner.with_upgrade_status(status)
+ end
- expect(Ci::Runner).to receive(:with_upgrade_status).with(status).and_call_original
+ expect(Ci::Runner).to receive(:with_upgrade_status).with(status).and_call_original
- result = execute
+ result = execute
- expect(result).to match_array(expected_result) if expected_result
+ expect(result).to match_array(expected_result) if expected_result
+ end
end
end
- end
- context 'set to an invalid value' do
- let(:upgrade_status) { :some_invalid_status }
+ context 'set to an invalid value' do
+ let(:upgrade_status) { :some_invalid_status }
- it 'raises ArgumentError' do
- expect { execute }.to raise_error(ArgumentError)
+ it 'raises ArgumentError' do
+ expect { execute }.to raise_error(ArgumentError)
+ end
end
- end
- context 'set to nil' do
- let(:upgrade_status) { nil }
+ context 'set to nil' do
+ let(:upgrade_status) { nil }
- it 'does not call with_upgrade_status' do
- expect(Ci::Runner).not_to receive(:with_upgrade_status)
+ it 'does not call with_upgrade_status' do
+ expect(Ci::Runner).not_to receive(:with_upgrade_status)
- expect(execute).to match_array(Ci::Runner.all)
+ expect(execute).to match_array(Ci::Runner.all)
+ end
end
end
- end
- context 'by status' do
- Ci::Runner::AVAILABLE_STATUSES.each do |status|
- it "calls the corresponding :#{status} scope on Ci::Runner" do
- expect(Ci::Runner).to receive(status.to_sym).and_call_original
+ context 'by status' do
+ Ci::Runner::AVAILABLE_STATUSES.each do |status|
+ it "calls the corresponding :#{status} scope on Ci::Runner" do
+ expect(Ci::Runner).to receive(status.to_sym).and_call_original
- described_class.new(current_user: admin, params: { status_status: status }).execute
+ described_class.new(current_user: admin, params: { status_status: status }).execute
+ end
end
end
- end
- context 'by active status' do
- it 'with active set as false calls the corresponding scope on Ci::Runner with false' do
- expect(Ci::Runner).to receive(:active).with(false).and_call_original
+ context 'by active status' do
+ it 'with active set as false calls the corresponding scope on Ci::Runner with false' do
+ expect(Ci::Runner).to receive(:active).with(false).and_call_original
- described_class.new(current_user: admin, params: { active: false }).execute
- end
+ described_class.new(current_user: admin, params: { active: false }).execute
+ end
- it 'with active set as true calls the corresponding scope on Ci::Runner with true' do
- expect(Ci::Runner).to receive(:active).with(true).and_call_original
+ it 'with active set as true calls the corresponding scope on Ci::Runner with true' do
+ expect(Ci::Runner).to receive(:active).with(true).and_call_original
- described_class.new(current_user: admin, params: { active: true }).execute
+ described_class.new(current_user: admin, params: { active: true }).execute
+ end
end
- end
- context 'by runner type' do
- it 'calls the corresponding scope on Ci::Runner' do
- expect(Ci::Runner).to receive(:project_type).and_call_original
+ context 'by runner type' do
+ it 'calls the corresponding scope on Ci::Runner' do
+ expect(Ci::Runner).to receive(:project_type).and_call_original
- described_class.new(current_user: admin, params: { type_type: 'project_type' }).execute
+ described_class.new(current_user: admin, params: { type_type: 'project_type' }).execute
+ end
end
- end
- context 'by tag_name' do
- it 'calls the corresponding scope on Ci::Runner' do
- expect(Ci::Runner).to receive(:tagged_with).with(%w[tag1 tag2]).and_call_original
+ context 'by tag_name' do
+ it 'calls the corresponding scope on Ci::Runner' do
+ expect(Ci::Runner).to receive(:tagged_with).with(%w[tag1 tag2]).and_call_original
- described_class.new(current_user: admin, params: { tag_name: %w[tag1 tag2] }).execute
+ described_class.new(current_user: admin, params: { tag_name: %w[tag1 tag2] }).execute
+ end
end
end
- end
- context 'sorting' do
- let_it_be(:runner1) { create :ci_runner, created_at: '2018-07-12 07:00', contacted_at: 1.minute.ago, token_expires_at: '2022-02-15 07:00' }
- let_it_be(:runner2) { create :ci_runner, created_at: '2018-07-12 08:00', contacted_at: 3.minutes.ago, token_expires_at: '2022-02-15 06:00' }
- let_it_be(:runner3) { create :ci_runner, created_at: '2018-07-12 09:00', contacted_at: 2.minutes.ago }
+ context 'sorting' do
+ let_it_be(:runner1) { create :ci_runner, created_at: '2018-07-12 07:00', contacted_at: 1.minute.ago, token_expires_at: '2022-02-15 07:00' }
+ let_it_be(:runner2) { create :ci_runner, created_at: '2018-07-12 08:00', contacted_at: 3.minutes.ago, token_expires_at: '2022-02-15 06:00' }
+ let_it_be(:runner3) { create :ci_runner, created_at: '2018-07-12 09:00', contacted_at: 2.minutes.ago }
- subject do
- described_class.new(current_user: admin, params: params).execute
- end
+ subject do
+ described_class.new(current_user: admin, params: params).execute
+ end
- shared_examples 'sorts by created_at descending' do
- it 'sorts by created_at descending' do
- is_expected.to eq [runner3, runner2, runner1]
+ shared_examples 'sorts by created_at descending' do
+ it 'sorts by created_at descending' do
+ is_expected.to eq [runner3, runner2, runner1]
+ end
end
- end
- context 'without sort param' do
- let(:params) { {} }
+ context 'without sort param' do
+ let(:params) { {} }
- it_behaves_like 'sorts by created_at descending'
- end
+ it_behaves_like 'sorts by created_at descending'
+ end
- %w(created_date created_at_desc).each do |sort|
- context "with sort param equal to #{sort}" do
- let(:params) { { sort: sort } }
+ %w(created_date created_at_desc).each do |sort|
+ context "with sort param equal to #{sort}" do
+ let(:params) { { sort: sort } }
- it_behaves_like 'sorts by created_at descending'
+ it_behaves_like 'sorts by created_at descending'
+ end
end
- end
- context 'with sort param equal to created_at_asc' do
- let(:params) { { sort: 'created_at_asc' } }
+ context 'with sort param equal to created_at_asc' do
+ let(:params) { { sort: 'created_at_asc' } }
- it 'sorts by created_at ascending' do
- is_expected.to eq [runner1, runner2, runner3]
+ it 'sorts by created_at ascending' do
+ is_expected.to eq [runner1, runner2, runner3]
+ end
end
- end
- context 'with sort param equal to contacted_asc' do
- let(:params) { { sort: 'contacted_asc' } }
+ context 'with sort param equal to contacted_asc' do
+ let(:params) { { sort: 'contacted_asc' } }
- it 'sorts by contacted_at ascending' do
- is_expected.to eq [runner2, runner3, runner1]
+ it 'sorts by contacted_at ascending' do
+ is_expected.to eq [runner2, runner3, runner1]
+ end
end
- end
- context 'with sort param equal to contacted_desc' do
- let(:params) { { sort: 'contacted_desc' } }
+ context 'with sort param equal to contacted_desc' do
+ let(:params) { { sort: 'contacted_desc' } }
- it 'sorts by contacted_at descending' do
- is_expected.to eq [runner1, runner3, runner2]
+ it 'sorts by contacted_at descending' do
+ is_expected.to eq [runner1, runner3, runner2]
+ end
end
- end
- context 'with sort param equal to token_expires_at_asc' do
- let(:params) { { sort: 'token_expires_at_asc' } }
+ context 'with sort param equal to token_expires_at_asc' do
+ let(:params) { { sort: 'token_expires_at_asc' } }
- it 'sorts by contacted_at ascending' do
- is_expected.to eq [runner2, runner1, runner3]
+ it 'sorts by contacted_at ascending' do
+ is_expected.to eq [runner2, runner1, runner3]
+ end
end
- end
- context 'with sort param equal to token_expires_at_desc' do
- let(:params) { { sort: 'token_expires_at_desc' } }
+ context 'with sort param equal to token_expires_at_desc' do
+ let(:params) { { sort: 'token_expires_at_desc' } }
- it 'sorts by contacted_at descending' do
- is_expected.to eq [runner3, runner1, runner2]
+ it 'sorts by contacted_at descending' do
+ is_expected.to eq [runner3, runner1, runner2]
+ end
end
end
end
- context 'by non admin user' do
+ shared_examples 'executes as normal user' do
it 'returns no runners' do
user = create :user
create :ci_runner, active: true
@@ -229,6 +231,24 @@ RSpec.describe Ci::RunnersFinder do
end
end
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it_behaves_like 'executes as admin'
+ end
+
+ context 'when admin mode setting is enabled' do
+ context 'when in admin mode', :enable_admin_mode do
+ it_behaves_like 'executes as admin'
+ end
+
+ context 'when not in admin mode' do
+ it_behaves_like 'executes as normal user'
+ end
+ end
+
+ context 'by non admin user' do
+ it_behaves_like 'executes as normal user'
+ end
+
context 'when user is nil' do
it 'returns no runners' do
user = nil
@@ -473,4 +493,153 @@ RSpec.describe Ci::RunnersFinder do
end
end
end
+
+ context 'project' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:other_project) { create(:project) }
+
+ let(:extra_params) { {} }
+ let(:params) { { project: project }.merge(extra_params).reject { |_, v| v.nil? } }
+
+ describe '#execute' do
+ subject { described_class.new(current_user: user, params: params).execute }
+
+ context 'with user as project admin' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'with project runners' do
+ let_it_be(:runner_project) { create(:ci_runner, :project, contacted_at: 7.minutes.ago, projects: [project]) }
+
+ it 'returns runners available to project' do
+ expect(subject).to match_array([runner_project])
+ end
+ end
+
+ context 'with ancestor group runners' do
+ let_it_be(:runner_instance) { create(:ci_runner, contacted_at: 13.minutes.ago) }
+ let_it_be(:runner_group) { create(:ci_runner, :group, contacted_at: 12.minutes.ago, groups: [group]) }
+
+ it 'returns runners available to project' do
+ expect(subject).to match_array([runner_instance, runner_group])
+ end
+ end
+
+ context 'with allowed shared runners' do
+ let_it_be(:runner_instance) { create(:ci_runner, :instance, contacted_at: 13.minutes.ago) }
+
+ it 'returns runners available to project' do
+ expect(subject).to match_array([runner_instance])
+ end
+ end
+
+ context 'with project, ancestor group, and allowed shared runners' do
+ let_it_be(:runner_project) { create(:ci_runner, :project, contacted_at: 7.minutes.ago, projects: [project]) }
+ let_it_be(:runner_group) { create(:ci_runner, :group, contacted_at: 12.minutes.ago, groups: [group]) }
+ let_it_be(:runner_instance) { create(:ci_runner, :instance, contacted_at: 13.minutes.ago) }
+
+ it 'returns runners available to project' do
+ expect(subject).to match_array([runner_project, runner_group, runner_instance])
+ end
+ end
+
+ context 'filtering' do
+ let_it_be(:runner_instance_inactive) { create(:ci_runner, :instance, active: false, contacted_at: 13.minutes.ago) }
+ let_it_be(:runner_instance_active) { create(:ci_runner, :instance, active: true, contacted_at: 13.minutes.ago) }
+ 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]) }
+
+ 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]) }
+ let_it_be(:runner_project_2) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, description: 'runner_project', projects: [project]) }
+ let_it_be(:runner_another_project) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, description: 'runner_project_search', projects: [other_project]) }
+
+ let(:extra_params) { { search: 'runner_project_search' } }
+
+ it 'returns the correct runner' do
+ expect(subject).to match_array([runner_project_1])
+ end
+ end
+
+ context 'by active status' do
+ let(:extra_params) { { active: false } }
+
+ it 'returns the correct runners' do
+ expect(subject).to match_array([runner_instance_inactive, runner_project_inactive])
+ end
+ end
+
+ context 'by status' do
+ let(:extra_params) { { status_status: 'paused' } }
+
+ it 'returns correct runner' do
+ expect(subject).to match_array([runner_instance_inactive, runner_project_inactive])
+ end
+ end
+
+ context 'by tag_name' do
+ let_it_be(:runner_project_1) { create(:ci_runner, :project, contacted_at: 3.minutes.ago, tag_list: %w[runner_tag], projects: [project]) }
+ let_it_be(:runner_project_2) { create(:ci_runner, :project, contacted_at: 3.minutes.ago, tag_list: %w[other_tag], projects: [project]) }
+ let_it_be(:runner_other_project) { create(:ci_runner, :project, contacted_at: 3.minutes.ago, tag_list: %w[runner_tag], projects: [other_project]) }
+
+ let(:extra_params) { { tag_name: %w[runner_tag] } }
+
+ it 'returns correct runner' do
+ expect(subject).to match_array([runner_project_1])
+ end
+ end
+
+ context 'by runner type' do
+ let(:extra_params) { { type_type: 'project_type' } }
+
+ it 'returns correct runners' do
+ expect(subject).to match_array([runner_project_active, runner_project_inactive])
+ end
+ end
+ end
+ end
+
+ context 'with user as project developer' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns no runners' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when user is nil' do
+ let_it_be(:user) { nil }
+
+ it 'returns no runners' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'with nil project_full_path' do
+ let(:project_full_path) { nil }
+
+ it 'returns no runners' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when on_demand_scans_runner_tags feature flag is disabled' do
+ before do
+ stub_feature_flags(on_demand_scans_runner_tags: false)
+ end
+
+ it 'returns no runners' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+ end
end
diff --git a/spec/finders/clusters/agent_tokens_finder_spec.rb b/spec/finders/clusters/agent_tokens_finder_spec.rb
index 619aca891c1..024e567a16e 100644
--- a/spec/finders/clusters/agent_tokens_finder_spec.rb
+++ b/spec/finders/clusters/agent_tokens_finder_spec.rb
@@ -5,24 +5,43 @@ require 'spec_helper'
RSpec.describe Clusters::AgentTokensFinder do
describe '#execute' do
let_it_be(:project) { create(:project) }
+ let_it_be(:agent) { create(:cluster_agent, project: project) }
let(:user) { create(:user, maintainer_projects: [project]) }
- let(:agent) { create(:cluster_agent, project: project) }
- let(:agent_id) { agent.id }
- let!(:matching_agent_tokens) do
+ let_it_be(:active_agent_tokens) do
[
create(:cluster_agent_token, agent: agent),
- create(:cluster_agent_token, :revoked, agent: agent)
+ create(:cluster_agent_token, agent: agent)
]
end
- subject(:execute) { described_class.new(project, user, agent_id).execute }
+ let_it_be(:revoked_agent_tokens) do
+ [
+ create(:cluster_agent_token, :revoked, agent: agent),
+ create(:cluster_agent_token, :revoked, agent: agent)
+ ]
+ end
- it 'returns the tokens of the specified agent' do
- # creating a token in a different agent to make sure it will not be included in the result
+ before_all do
+ # set up a token under a different agent as a way to verify
+ # that only tokens of a given agent are included in the result
create(:cluster_agent_token, agent: create(:cluster_agent))
+ end
+
+ subject(:execute) { described_class.new(agent, user).execute }
+
+ it { is_expected.to match_array(active_agent_tokens + revoked_agent_tokens) }
- expect(execute).to match_array(matching_agent_tokens)
+ context 'when filtering by status=active' do
+ subject(:execute) { described_class.new(agent, user, status: 'active').execute }
+
+ it { is_expected.to match_array(active_agent_tokens) }
+ end
+
+ context 'when filtering by status=revoked' do
+ subject(:execute) { described_class.new(agent, user, status: 'revoked').execute }
+
+ it { is_expected.to match_array(revoked_agent_tokens) }
end
context 'when user does not have permission' do
@@ -32,16 +51,20 @@ RSpec.describe Clusters::AgentTokensFinder do
project.add_reporter(user)
end
- it 'raises an error' do
- expect { execute }.to raise_error(ActiveRecord::RecordNotFound)
- end
+ it { is_expected.to eq ::Clusters::AgentToken.none }
end
- context 'when agent does not exist' do
- let(:agent_id) { non_existing_record_id }
+ context 'when current_user is nil' do
+ it 'returns an empty list' do
+ result = described_class.new(agent, nil).execute
+ expect(result).to eq ::Clusters::AgentToken.none
+ end
+ end
- it 'raises an error' do
- expect { execute }.to raise_error(ActiveRecord::RecordNotFound)
+ context 'when agent is nil' do
+ it 'returns an empty list' do
+ result = described_class.new(nil, user).execute
+ expect(result).to eq ::Clusters::AgentToken.none
end
end
end
diff --git a/spec/finders/environments/environments_finder_spec.rb b/spec/finders/environments/environments_finder_spec.rb
index 04fbd4067b4..df66bbdc235 100644
--- a/spec/finders/environments/environments_finder_spec.rb
+++ b/spec/finders/environments/environments_finder_spec.rb
@@ -51,15 +51,35 @@ RSpec.describe Environments::EnvironmentsFinder do
end
context 'with search and states' do
+ let_it_be(:environment_available_b) { create(:environment, :available, name: 'test/foldered-env', project: project) }
+
it 'searches environments by name and state' do
result = described_class.new(project, user, search: 'test', states: :available).execute
- expect(result).to contain_exactly(environment_available)
+ expect(result).to contain_exactly(environment_available, environment_available_b)
+ end
+
+ it 'searches environments by name inside folder and state' do
+ result = described_class.new(project, user, search: 'folder', states: :available).execute
+
+ expect(result).to contain_exactly(environment_available_b)
+ end
+
+ context 'when enable_environments_search_within_folder FF is disabled' do
+ before do
+ stub_feature_flags(enable_environments_search_within_folder: false)
+ end
+
+ it 'ignores name inside folder' do
+ result = described_class.new(project, user, search: 'folder', states: :available).execute
+
+ expect(result).to be_empty
+ end
end
end
context 'with id' do
- it 'searches environments by name and state' do
+ it 'searches environments by name and id' do
result = described_class.new(project, user, search: 'test', environment_ids: [environment_available.id]).execute
expect(result).to contain_exactly(environment_available)
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 704171a737b..43d66d285fa 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IssuesFinder do
+RSpec.describe IssuesFinder, feature_category: :team_planning do
include_context 'IssuesFinder context'
it_behaves_like 'issues or work items finder', :issue, 'IssuesFinder#execute context'
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 11de19cfdbc..61be90b267a 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -328,6 +328,16 @@ RSpec.describe NotesFinder do
it 'returns the commit' do
expect(subject.target).to eq(commit)
end
+
+ context 'user does not have permission to read_code' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_code, project).and_return false
+ end
+
+ it 'returns nil' do
+ expect(subject.target).to be_nil
+ end
+ end
end
context 'target_iid' do
diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb
index 21380cb6632..bcd5aef84f9 100644
--- a/spec/finders/personal_access_tokens_finder_spec.rb
+++ b/spec/finders/personal_access_tokens_finder_spec.rb
@@ -2,359 +2,320 @@
require 'spec_helper'
-RSpec.describe PersonalAccessTokensFinder do
- def finder(options = {}, current_user = nil)
- described_class.new(options, current_user)
- end
-
- describe '# searches PATs' do
- using RSpec::Parameterized::TableSyntax
+RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode do
+ using RSpec::Parameterized::TableSyntax
- let_it_be(:time_token) do
- create(:personal_access_token, created_at: DateTime.new(2022, 01, 02),
- last_used_at: DateTime.new(2022, 01, 02))
+ describe '#execute' do
+ let(:admin) { create(:admin) }
+ let(:user) { create(:user) }
+ let(:other_user) { create(:user) }
+ let(:project_bot) { create(:user, :project_bot) }
+
+ let!(: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'),
+ expired: create(:personal_access_token, :expired, user: user),
+ revoked: create(:personal_access_token, :revoked, user: user),
+ active_impersonation: create(:personal_access_token, :impersonation, user: user),
+ expired_impersonation: create(:personal_access_token, :expired, :impersonation, user: user),
+ revoked_impersonation: create(:personal_access_token, :revoked, :impersonation, user: user),
+ bot: create(:personal_access_token, user: project_bot)
+ }
end
- let_it_be(:name_token) { create(:personal_access_token, name: 'test_1') }
-
- let_it_be(:impersonated_token) do
- create(:personal_access_token, :impersonation,
- created_at: DateTime.new(2022, 01, 02),
- last_used_at: DateTime.new(2022, 01, 02),
- name: 'imp_token'
- )
- end
+ let(:params) { {} }
+ let(:current_user) { admin }
- shared_examples 'finding tokens by user and options' do
- subject { finder(option, user).execute }
+ subject { described_class.new(params, current_user).execute }
- it 'finds exactly' do
- subject
+ describe 'by current user' do
+ context 'with no user' do
+ let(:current_user) { nil }
- is_expected.to contain_exactly(*result)
+ it 'returns all tokens' do
+ is_expected.to match_array(tokens.values)
+ end
end
- end
- context 'by' do
- where(:option, :user, :result) do
- { created_before: DateTime.new(2022, 01, 03) } | create(:admin) | lazy { [time_token, impersonated_token] }
- { created_after: DateTime.new(2022, 01, 01) } | create(:admin) | lazy { [time_token, name_token, impersonated_token] }
- { last_used_before: DateTime.new(2022, 01, 03) } | create(:admin) | lazy { [time_token, impersonated_token] }
- { last_used_before: DateTime.new(2022, 01, 03) } | create(:admin) | lazy { [time_token, impersonated_token] }
- { impersonation: true } | create(:admin) | lazy { [impersonated_token] }
- { search: 'test' } | create(:admin) | lazy { [name_token] }
- end
+ context 'with admin' do
+ let(:current_user) { admin }
- with_them do
- it_behaves_like 'finding tokens by user and options'
- end
- end
- end
-
- describe '#execute' do
- let(:user) { create(:user) }
- let(:params) { {} }
- let(:current_user) { nil }
- let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
- let!(:expired_personal_access_token) { create(:personal_access_token, :expired, user: user) }
- let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
- let!(:active_impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
- let!(:expired_impersonation_token) { create(:personal_access_token, :expired, :impersonation, user: user) }
- let!(:revoked_impersonation_token) { create(:personal_access_token, :revoked, :impersonation, user: user) }
- let!(:project_bot) { create(:user, :project_bot) }
- let!(:project_member) { create(:project_member, user: project_bot) }
- let!(:project_access_token) { create(:personal_access_token, user: project_bot) }
-
- subject { finder(params, current_user).execute }
-
- context 'when current_user is defined' do
- let(:current_user) { create(:admin) }
- let(:params) { { user: user } }
-
- context 'current_user is allowed to read PATs' do
- it do
- is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token,
- revoked_personal_access_token, expired_personal_access_token,
- revoked_impersonation_token, expired_impersonation_token)
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it 'returns all tokens' do
+ is_expected.to match_array(tokens.values)
+ end
end
- end
- context 'current_user is not allowed to read PATs' do
- let(:current_user) { create(:user) }
+ context 'when admin mode setting is enabled' do
+ context 'when in admin mode', :enable_admin_mode do
+ it 'returns all tokens' do
+ is_expected.to match_array(tokens.values)
+ end
+ end
+
+ context 'when not in admin mode' do
+ before do
+ allow_next_instance_of(Gitlab::Auth::CurrentUserMode) do |current_user_mode|
+ allow(current_user_mode).to receive(:admin_mode?).and_return(false)
+ end
+ end
- it { is_expected.to be_empty }
+ it 'returns no tokens' do
+ is_expected.to be_empty
+ end
+ end
+ end
end
- context 'when user param is not set' do
- let(:params) { {} }
+ context 'when user can read user personal access tokens' do
+ let(:params) { { user: user } }
+ let(:current_user) { user }
- it do
- is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token,
- revoked_personal_access_token, expired_personal_access_token,
- revoked_impersonation_token, expired_impersonation_token, project_access_token)
+ it 'returns tokens of user' do
+ is_expected.to contain_exactly(*user.personal_access_tokens)
end
+ end
- context 'when current_user is not an administrator' do
- let(:current_user) { create(:user) }
+ context 'when user can not read user personal access tokens' do
+ let(:params) { { user: other_user } }
+ let(:current_user) { user }
- it { is_expected.to be_empty }
+ it 'returns no tokens' do
+ is_expected.to be_empty
end
end
end
- describe 'without user' do
- it do
- is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token,
- revoked_personal_access_token, expired_personal_access_token,
- revoked_impersonation_token, expired_impersonation_token, project_access_token)
+ describe 'by user' do
+ where(:by_user, :expected_tokens) do
+ nil | tokens.keys
+ ref(:user) | [:active, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation]
+ ref(:other_user) | [:active_other]
+ ref(:admin) | []
end
- describe 'with users' do
- let(:user2) { create(:user) }
-
- before do
- create(:personal_access_token, user: user2)
- create(:personal_access_token, :expired, user: user2)
- create(:personal_access_token, :revoked, user: user2)
- create(:personal_access_token, :impersonation, user: user2)
- create(:personal_access_token, :expired, :impersonation, user: user2)
- create(:personal_access_token, :revoked, :impersonation, user: user2)
+ with_them do
+ let(:params) { { user: by_user } }
- params[:users] = [user]
+ it 'returns tokens by user' do
+ is_expected.to match_array(tokens.values_at(*expected_tokens))
end
-
- it {
- is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token,
- revoked_personal_access_token, expired_personal_access_token,
- revoked_impersonation_token, expired_impersonation_token)
- }
end
+ end
- describe 'with sort order' do
- before do
- params[:sort] = 'id_asc'
- end
-
- it 'sorts records as per the specified sort order' do
- expect(subject).to match_array(PersonalAccessToken.all.order(id: :asc))
- end
+ describe 'by users' do
+ where(:by_users, :expected_tokens) do
+ nil | 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]
+ [] | [] # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
end
- describe 'without impersonation' do
- before do
- params[:impersonation] = false
- end
-
- it { is_expected.to contain_exactly(active_personal_access_token, revoked_personal_access_token, expired_personal_access_token, project_access_token) }
-
- describe 'with active state' do
- before do
- params[:state] = 'active'
- end
-
- it { is_expected.to contain_exactly(active_personal_access_token, project_access_token) }
- end
-
- describe 'with inactive state' do
- before do
- params[:state] = 'inactive'
- end
+ with_them do
+ let(:params) { { users: by_users } }
- it { is_expected.to contain_exactly(revoked_personal_access_token, expired_personal_access_token) }
+ it 'returns tokens by users' do
+ is_expected.to match_array(tokens.values_at(*expected_tokens))
end
end
+ end
- describe 'with impersonation' do
- before do
- params[:impersonation] = true
- end
-
- it { is_expected.to contain_exactly(active_impersonation_token, revoked_impersonation_token, expired_impersonation_token) }
-
- describe 'with active state' do
- before do
- params[:state] = 'active'
- end
-
- it { is_expected.to contain_exactly(active_impersonation_token) }
- end
+ describe 'by impersonation' do
+ where(:by_impersonation, :expected_tokens) do
+ nil | tokens.keys
+ true | [:active_impersonation, :expired_impersonation, :revoked_impersonation]
+ false | [:active, :active_other, :expired, :revoked, :bot]
+ 'other' | tokens.keys
+ end
- describe 'with inactive state' do
- before do
- params[:state] = 'inactive'
- end
+ with_them do
+ let(:params) { { impersonation: by_impersonation } }
- it { is_expected.to contain_exactly(revoked_impersonation_token, expired_impersonation_token) }
+ it 'returns tokens by impersonation' do
+ is_expected.to match_array(tokens.values_at(*expected_tokens))
end
end
+ end
- describe 'with active state' do
- before do
- params[:state] = 'active'
- end
-
- it { is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token, project_access_token) }
+ describe 'by state' do
+ where(:by_state, :expected_tokens) do
+ nil | tokens.keys
+ 'active' | [:active, :active_other, :active_impersonation, :bot]
+ 'inactive' | [:expired, :revoked, :expired_impersonation, :revoked_impersonation]
+ 'other' | tokens.keys
end
- describe 'with inactive state' do
- before do
- params[:state] = 'inactive'
- end
+ with_them do
+ let(:params) { { state: by_state } }
- it do
- is_expected.to contain_exactly(expired_personal_access_token, revoked_personal_access_token,
- expired_impersonation_token, revoked_impersonation_token)
+ it 'returns tokens by state' do
+ is_expected.to match_array(tokens.values_at(*expected_tokens))
end
end
+ end
- describe 'with id' do
- subject { finder(params).find_by_id(active_personal_access_token.id) }
-
- it { is_expected.to eq(active_personal_access_token) }
+ describe 'by owner type' do
+ where(:by_owner_type, :expected_tokens) do
+ nil | tokens.keys
+ 'human' | [:active, :active_other, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation]
+ 'other' | tokens.keys
+ end
- describe 'with impersonation' do
- before do
- params[:impersonation] = true
- end
+ with_them do
+ let(:params) { { owner_type: by_owner_type } }
- it { is_expected.to be_nil }
+ it 'returns tokens by owner type' do
+ is_expected.to match_array(tokens.values_at(*expected_tokens))
end
end
+ end
- describe 'with token' do
- subject { finder(params).find_by_token(active_personal_access_token.token) }
-
- it { is_expected.to eq(active_personal_access_token) }
+ describe 'by revoked state' do
+ where(:by_revoked_state, :expected_tokens) do
+ nil | [:active, :active_other, :expired, :active_impersonation, :expired_impersonation, :bot]
+ true | [:revoked, :revoked_impersonation]
+ false | [:active, :active_other, :expired, :active_impersonation, :expired_impersonation, :bot]
+ end
- describe 'with impersonation' do
- before do
- params[:impersonation] = true
- end
+ with_them do
+ let(:params) { { revoked: by_revoked_state } }
- it { is_expected.to be_nil }
+ it 'returns tokens by revoked state' do
+ is_expected.to match_array(tokens.values_at(*expected_tokens))
end
end
end
- describe 'with user' do
- let(:user2) { create(:user) }
- let!(:other_user_active_personal_access_token) { create(:personal_access_token, user: user2) }
- let!(:other_user_expired_personal_access_token) { create(:personal_access_token, :expired, user: user2) }
- let!(:other_user_revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user2) }
- let!(:other_user_active_impersonation_token) { create(:personal_access_token, :impersonation, user: user2) }
- let!(:other_user_expired_impersonation_token) { create(:personal_access_token, :expired, :impersonation, user: user2) }
- let!(:other_user_revoked_impersonation_token) { create(:personal_access_token, :revoked, :impersonation, user: user2) }
-
+ describe 'by created date' do
before do
- params[:user] = user
- end
-
- it do
- is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token,
- revoked_personal_access_token, expired_personal_access_token,
- revoked_impersonation_token, expired_impersonation_token)
+ tokens[:active_other].update!(created_at: 5.days.ago)
end
- describe 'filtering human tokens' do
- before do
- params[:owner_type] = 'human'
+ describe 'by created before' do
+ where(:by_created_before, :expected_tokens) do
+ 6.days.ago | []
+ 2.days.ago | [:active_other]
+ 2.days.from_now | tokens.keys
end
- it { is_expected.not_to include(project_access_token) }
+ with_them do
+ let(:params) { { created_before: by_created_before } }
+
+ it 'returns tokens by created before' do
+ is_expected.to match_array(tokens.values_at(*expected_tokens))
+ end
+ end
end
- describe 'without impersonation' do
- before do
- params[:impersonation] = false
+ describe 'by created after' do
+ where(:by_created_after, :expected_tokens) do
+ 6.days.ago | tokens.keys
+ 2.days.ago | [:active, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation, :bot]
+ 2.days.from_now | []
end
- it { is_expected.to contain_exactly(active_personal_access_token, revoked_personal_access_token, expired_personal_access_token) }
+ with_them do
+ let(:params) { { created_after: by_created_after } }
- describe 'with active state' do
- before do
- params[:state] = 'active'
+ it 'returns tokens by created before' do
+ is_expected.to match_array(tokens.values_at(*expected_tokens))
end
-
- it { is_expected.to contain_exactly(active_personal_access_token) }
end
+ end
+ end
- describe 'with inactive state' do
- before do
- params[:state] = 'inactive'
- end
-
- it { is_expected.to contain_exactly(revoked_personal_access_token, expired_personal_access_token) }
- end
+ describe 'by last used date' do
+ before do
+ PersonalAccessToken.update_all(last_used_at: Time.now)
+ tokens[:active_other].update!(last_used_at: 5.days.ago)
end
- describe 'with impersonation' do
- before do
- params[:impersonation] = true
+ describe 'by last used before' do
+ where(:by_last_used_before, :expected_tokens) do
+ 6.days.ago | []
+ 2.days.ago | [:active_other]
+ 2.days.from_now | tokens.keys
end
- it { is_expected.to contain_exactly(active_impersonation_token, revoked_impersonation_token, expired_impersonation_token) }
+ with_them do
+ let(:params) { { last_used_before: by_last_used_before } }
- describe 'with active state' do
- before do
- params[:state] = 'active'
+ it 'returns tokens by last used before' do
+ is_expected.to match_array(tokens.values_at(*expected_tokens))
end
+ end
+ end
- it { is_expected.to contain_exactly(active_impersonation_token) }
+ describe 'by last used after' do
+ where(:by_last_used_after, :expected_tokens) do
+ 6.days.ago | tokens.keys
+ 2.days.ago | [:active, :expired, :revoked, :active_impersonation, :expired_impersonation, :revoked_impersonation, :bot]
+ 2.days.from_now | []
end
- describe 'with inactive state' do
- before do
- params[:state] = 'inactive'
- end
+ with_them do
+ let(:params) { { last_used_after: by_last_used_after } }
- it { is_expected.to contain_exactly(revoked_impersonation_token, expired_impersonation_token) }
+ it 'returns tokens by last used after' do
+ is_expected.to match_array(tokens.values_at(*expected_tokens))
+ end
end
end
+ end
- describe 'with active state' do
- before do
- params[:state] = 'active'
- end
-
- it { is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token) }
+ describe 'by search' do
+ where(:by_search, :expected_tokens) do
+ nil | tokens.keys
+ 'my_pat' | [:active, :active_other]
+ 'other' | []
end
- describe 'with inactive state' do
- before do
- params[:state] = 'inactive'
- end
+ with_them do
+ let(:params) { { search: by_search } }
- it do
- is_expected.to contain_exactly(expired_personal_access_token, revoked_personal_access_token,
- expired_impersonation_token, revoked_impersonation_token)
+ it 'returns tokens by search' do
+ is_expected.to match_array(tokens.values_at(*expected_tokens))
end
end
+ end
- describe 'with id' do
- subject { finder(params).find_by_id(active_personal_access_token.id) }
-
- it { is_expected.to eq(active_personal_access_token) }
+ describe 'sort' do
+ where(:sort, :expected_tokens) do
+ nil | 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
+ end
- describe 'with impersonation' do
- before do
- params[:impersonation] = true
- end
+ with_them do
+ let(:params) { { sort: sort } }
- it { is_expected.to be_nil }
+ it 'returns ordered tokens' do
+ expect(subject.map(&:id)).to eq(tokens.values_at(*expected_tokens).map(&:id))
end
end
+ end
- describe 'with token' do
- subject { finder(params).find_by_token(active_personal_access_token.token) }
+ describe 'delegates' do
+ subject { described_class.new(params, current_user) }
- it { is_expected.to eq(active_personal_access_token) }
+ describe '#find_by_id' do
+ it 'returns token by id' do
+ expect(subject.find_by_id(tokens[:active].id)).to eq(tokens[:active])
+ end
+ end
- describe 'with impersonation' do
- before do
- params[:impersonation] = true
- end
+ describe '#find_by_token' do
+ it 'returns token by token' do
+ expect(subject.find_by_token(tokens[:active].token)).to eq(tokens[:active])
+ end
+ end
- it { is_expected.to be_nil }
+ describe '#find' do
+ it 'returns token by id' do
+ expect(subject.find(tokens[:active].id)).to eq(tokens[:active])
end
end
end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 02153715eac..9fecbfb71fc 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -392,6 +392,23 @@ RSpec.describe ProjectsFinder do
it { is_expected.to match_array([project]) }
end
+ describe 'filter by language' do
+ let_it_be(:ruby) { create(:programming_language, name: 'Ruby') }
+ let_it_be(:repository_language) { create(:repository_language, project: internal_project, programming_language: ruby) }
+
+ let(:params) { { language: ruby.id } }
+
+ it { is_expected.to match_array([internal_project]) }
+
+ context 'when project_language_search feature flag disabled' do
+ before do
+ stub_feature_flags(project_language_search: false)
+ end
+
+ it { is_expected.to match_array([internal_project, public_project]) }
+ end
+ end
+
describe 'sorting' do
let_it_be(:more_projects) do
[
diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb
index 0bf9b228c8a..2af23c466fb 100644
--- a/spec/finders/tags_finder_spec.rb
+++ b/spec/finders/tags_finder_spec.rb
@@ -68,6 +68,14 @@ RSpec.describe TagsFinder do
expect(result.count).to eq(1)
end
+ it 'filters tags by name with wildcard' do
+ result = load_tags({ search: 'v1.*.0' })
+
+ expect(result.first.name).to eq('v1.0.0')
+ expect(result.second.name).to eq('v1.1.0')
+ expect(result.count).to eq(2)
+ end
+
it 'filters tags by nonexistent name that begins with' do
result = load_tags({ search: '^nope' })
@@ -79,6 +87,11 @@ RSpec.describe TagsFinder do
expect(result.count).to eq(0)
end
+ it 'filters tags by nonexistent name with wildcard' do
+ result = load_tags({ search: 'n*e' })
+ expect(result.count).to eq(0)
+ end
+
context 'when search is not a string' do
it 'returns no matches' do
result = load_tags({ search: { 'a' => 'b' } })
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index 5611a67e977..bcead6b0170 100644
--- a/spec/finders/todos_finder_spec.rb
+++ b/spec/finders/todos_finder_spec.rb
@@ -327,9 +327,9 @@ RSpec.describe TodosFinder do
it 'returns the expected types' do
expected_result =
if Gitlab.ee?
- %w[Epic Issue MergeRequest DesignManagement::Design AlertManagement::Alert]
+ %w[Epic Issue WorkItem MergeRequest DesignManagement::Design AlertManagement::Alert]
else
- %w[Issue MergeRequest DesignManagement::Design AlertManagement::Alert]
+ %w[Issue WorkItem MergeRequest DesignManagement::Design AlertManagement::Alert]
end
expect(described_class.todo_types).to contain_exactly(*expected_result)
diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb
index 271dce44db7..5cf845a87b2 100644
--- a/spec/finders/users_finder_spec.rb
+++ b/spec/finders/users_finder_spec.rb
@@ -8,9 +8,7 @@ RSpec.describe UsersFinder do
let_it_be(:project_bot) { create(:user, :project_bot) }
- context 'with a normal user' do
- let_it_be(:user) { create(:user) }
-
+ shared_examples 'executes users finder as normal user' do
it 'returns searchable users' do
users = described_class.new(user).execute
@@ -97,37 +95,35 @@ RSpec.describe UsersFinder do
end
end
- context 'with an admin user', :enable_admin_mode do
- let_it_be(:admin) { create(:admin) }
-
+ shared_examples 'executes users finder as admin' do
it 'filters by external users' do
- users = described_class.new(admin, external: true).execute
+ users = described_class.new(user, external: true).execute
expect(users).to contain_exactly(external_user)
end
it 'returns all users' do
- users = described_class.new(admin).execute
+ users = described_class.new(user).execute
- expect(users).to contain_exactly(admin, normal_user, blocked_user, unconfirmed_user, banned_user, external_user, omniauth_user, internal_user, admin_user, project_bot)
+ expect(users).to contain_exactly(user, normal_user, blocked_user, unconfirmed_user, banned_user, external_user, omniauth_user, internal_user, admin_user, project_bot)
end
it 'filters by blocked users' do
- users = described_class.new(admin, blocked: true).execute
+ users = described_class.new(user, blocked: true).execute
expect(users).to contain_exactly(blocked_user)
end
it 'filters by active users' do
- users = described_class.new(admin, active: true).execute
+ users = described_class.new(user, active: true).execute
- expect(users).to contain_exactly(admin, normal_user, unconfirmed_user, external_user, omniauth_user, admin_user, project_bot)
+ expect(users).to contain_exactly(user, normal_user, unconfirmed_user, external_user, omniauth_user, admin_user, project_bot)
end
it 'returns only admins' do
- users = described_class.new(admin, admins: true).execute
+ users = described_class.new(user, admins: true).execute
- expect(users).to contain_exactly(admin, admin_user)
+ expect(users).to contain_exactly(user, admin_user)
end
it 'filters by custom attributes' do
@@ -137,7 +133,7 @@ RSpec.describe UsersFinder do
create :user_custom_attribute, user: internal_user, key: 'foo', value: 'foo'
users = described_class.new(
- admin,
+ user,
custom_attributes: { foo: 'foo', bar: 'bar' }
).execute
@@ -145,10 +141,34 @@ RSpec.describe UsersFinder do
end
it 'filters by private emails search' do
- users = described_class.new(admin, search: normal_user.email).execute
+ users = described_class.new(user, search: normal_user.email).execute
expect(users).to contain_exactly(normal_user)
end
end
+
+ context 'with a normal user' do
+ let_it_be(:user) { create(:user) }
+
+ it_behaves_like 'executes users finder as normal user'
+ end
+
+ context 'with an admin user' do
+ let_it_be(:user) { create(:admin) }
+
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it_behaves_like 'executes users finder as admin'
+ end
+
+ context 'when admin mode setting is enabled' do
+ context 'when in admin mode', :enable_admin_mode do
+ it_behaves_like 'executes users finder as admin'
+ end
+
+ context 'when not in admin mode' do
+ it_behaves_like 'executes users finder as normal user'
+ end
+ end
+ end
end
end
diff --git a/spec/finders/work_items/work_items_finder_spec.rb b/spec/finders/work_items/work_items_finder_spec.rb
index fe400688a23..ab8a9ba9204 100644
--- a/spec/finders/work_items/work_items_finder_spec.rb
+++ b/spec/finders/work_items/work_items_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::WorkItemsFinder do
+RSpec.describe WorkItems::WorkItemsFinder, feature_category: :team_planning do
using RSpec::Parameterized::TableSyntax
include_context 'WorkItemsFinder context'
diff --git a/spec/fixtures/api/schemas/branch.json b/spec/fixtures/api/schemas/branch.json
index 0bb74577010..02389a1b979 100644
--- a/spec/fixtures/api/schemas/branch.json
+++ b/spec/fixtures/api/schemas/branch.json
@@ -1,12 +1,17 @@
{
"type": "object",
- "required" : [
+ "required": [
"name",
"url"
],
- "properties" : {
- "name": { "type": "string" },
- "url": { "type": "uri" }
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ }
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index 6f9535286ed..efc609b3c3f 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -1,55 +1,118 @@
{
"type": "object",
- "required" : [
+ "required": [
"status",
"applications"
],
- "properties" : {
- "status": { "type": "string" },
- "status_reason": { "type": ["string", "null"] },
+ "properties": {
+ "status": {
+ "type": "string"
+ },
+ "status_reason": {
+ "$ref": "types/nullable_string.json"
+ },
"applications": {
"type": "array",
- "items": { "$ref": "#/definitions/application_status" }
+ "items": {
+ "$ref": "#/definitions/application_status"
+ }
}
},
"additionalProperties": false,
"definitions": {
"application_status": {
"type": "object",
+ "required": [
+ "name",
+ "status"
+ ],
"additionalProperties": false,
- "properties" : {
- "name": { "type": "string" },
+ "properties": {
+ "name": {
+ "type": "string"
+ },
"status": {
- "type": {
- "enum": [
- "installable",
- "scheduled",
- "installing",
- "installed",
- "errored"
- ]
- }
+ "type": "string",
+ "enum": [
+ "installable",
+ "scheduled",
+ "installing",
+ "installed",
+ "errored",
+ "not_installable"
+ ]
+ },
+ "version": {
+ "type": "string"
+ },
+ "status_reason": {
+ "$ref": "types/nullable_string.json"
+ },
+ "external_ip": {
+ "$ref": "types/nullable_string.json"
+ },
+ "external_hostname": {
+ "$ref": "types/nullable_string.json"
+ },
+ "hostname": {
+ "$ref": "types/nullable_string.json"
+ },
+ "email": {
+ "$ref": "types/nullable_string.json"
+ },
+ "stack": {
+ "$ref": "types/nullable_string.json"
+ },
+ "host": {
+ "$ref": "types/nullable_string.json"
+ },
+ "port": {
+ "type": "integer"
+ },
+ "protocol": {
+ "type": "integer"
+ },
+ "update_available": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "can_uninstall": {
+ "type": "boolean"
},
- "version": { "type": "string" },
- "status_reason": { "type": ["string", "null"] },
- "external_ip": { "type": ["string", "null"] },
- "external_hostname": { "type": ["string", "null"] },
- "hostname": { "type": ["string", "null"] },
- "email": { "type": ["string", "null"] },
- "stack": { "type": ["string", "null"] },
- "host": {"type": ["string", "null"]},
- "port": {"type": ["integer", "514"]},
- "protocol": {"type": ["integer", "0"]},
- "update_available": { "type": ["boolean", "null"] },
- "can_uninstall": { "type": "boolean" },
"available_domains": {
"type": "array",
- "items": { "$ref": "#/definitions/domain" }
+ "items": {
+ "$ref": "#/definitions/domain"
+ }
},
- "pages_domain": { "type": [ { "$ref": "#/definitions/domain" }, "null"] }
- },
- "required" : [ "name", "status" ]
+ "pages_domain": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/definitions/domain"
+ }
+ ]
+ }
+ }
},
- "domain": { "id": "integer", "domain": "string" }
+ "domain": {
+ "type": "object",
+ "required": [
+ "id",
+ "domain"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "domain": {
+ "type": "string"
+ }
+ }
+ }
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/conflicts.json b/spec/fixtures/api/schemas/conflicts.json
index a947783d505..f8acac9f074 100644
--- a/spec/fixtures/api/schemas/conflicts.json
+++ b/spec/fixtures/api/schemas/conflicts.json
@@ -8,16 +8,29 @@
"files"
],
"properties": {
- "commit_message": {"type": "string"},
- "commit_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
- "source_branch": {"type": "string"},
- "target_branch": {"type": "string"},
+ "commit_message": {
+ "type": "string"
+ },
+ "commit_sha": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{40}$"
+ },
+ "source_branch": {
+ "type": "string"
+ },
+ "target_branch": {
+ "type": "string"
+ },
"files": {
"type": "array",
"items": {
"oneOf": [
- { "$ref": "#/definitions/conflict-text-with-sections" },
- { "$ref": "#/definitions/conflict-text-for-editor" }
+ {
+ "$ref": "#/definitions/conflict-text-with-sections"
+ },
+ {
+ "$ref": "#/definitions/conflict-text-for-editor"
+ }
]
}
}
@@ -32,15 +45,25 @@
"blob_path"
],
"properties": {
- "old_path": {"type": "string"},
- "new_path": {"type": "string"},
- "blob_icon": {"type": "string"},
- "blob_path": {"type": "string"}
+ "old_path": {
+ "type": "string"
+ },
+ "new_path": {
+ "type": "string"
+ },
+ "blob_icon": {
+ "type": "string"
+ },
+ "blob_path": {
+ "type": "string"
+ }
}
},
"conflict-text-for-editor": {
"allOf": [
- {"$ref": "#/definitions/conflict-base"},
+ {
+ "$ref": "#/definitions/conflict-base"
+ },
{
"type": "object",
"required": [
@@ -48,15 +71,25 @@
"content_path"
],
"properties": {
- "type": {"type": {"enum": ["text-editor"]}},
- "content_path": {"type": "string"}
+ "type": {
+ "type": "string",
+ "enum": [
+ "text",
+ "text-editor"
+ ]
+ },
+ "content_path": {
+ "type": "string"
+ }
}
}
]
},
"conflict-text-with-sections": {
"allOf": [
- {"$ref": "#/definitions/conflict-base"},
+ {
+ "$ref": "#/definitions/conflict-base"
+ },
{
"type": "object",
"required": [
@@ -65,14 +98,25 @@
"sections"
],
"properties": {
- "type": {"type": {"enum": ["text"]}},
- "content_path": {"type": "string"},
+ "type": {
+ "type": "string",
+ "enum": [
+ "text"
+ ]
+ },
+ "content_path": {
+ "type": "string"
+ },
"sections": {
"type": "array",
"items": {
"oneOf": [
- { "$ref": "#/definitions/section-context" },
- { "$ref": "#/definitions/section-conflict" }
+ {
+ "$ref": "#/definitions/section-context"
+ },
+ {
+ "$ref": "#/definitions/section-conflict"
+ }
]
}
}
@@ -87,7 +131,9 @@
"lines"
],
"properties": {
- "conflict": {"type": "boolean"},
+ "conflict": {
+ "type": "boolean"
+ },
"lines": {
"type": "array",
"items": {
@@ -99,11 +145,21 @@
"rich_text"
],
"properties": {
- "type": {"type": "string"},
- "old_line": {"type": "string"},
- "new_line": {"type": "string"},
- "text": {"type": "string"},
- "rich_text": {"type": "string"}
+ "type": {
+ "type": "string"
+ },
+ "old_line": {
+ "type": "string"
+ },
+ "new_line": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "rich_text": {
+ "type": "string"
+ }
}
}
}
@@ -111,27 +167,39 @@
},
"section-context": {
"allOf": [
- {"$ref": "#/definitions/section-base"},
+ {
+ "$ref": "#/definitions/section-base"
+ },
{
"type": "object",
"properties": {
- "conflict": {"enum": [false]}
+ "conflict": {
+ "type": "boolean"
+ }
}
}
]
},
"section-conflict": {
"allOf": [
- {"$ref": "#/definitions/section-base"},
+ {
+ "$ref": "#/definitions/section-base"
+ },
{
"type": "object",
- "required": ["id"],
+ "required": [
+ "id"
+ ],
"properties": {
- "conflict": {"enum": [true]},
- "id": {"type": "string"}
+ "conflict": {
+ "type": "boolean"
+ },
+ "id": {
+ "type": "string"
+ }
}
}
]
}
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/discussion.json b/spec/fixtures/api/schemas/entities/discussion.json
index da2d2a83a8d..45271926547 100644
--- a/spec/fixtures/api/schemas/entities/discussion.json
+++ b/spec/fixtures/api/schemas/entities/discussion.json
@@ -1,34 +1,75 @@
{
"type": "object",
- "required" : [
+ "required": [
"id",
"notes",
"individual_note"
],
- "properties" : {
- "id": { "type": "string" },
- "individual_note": { "type": "boolean" },
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "individual_note": {
+ "type": "boolean"
+ },
"notes": {
"type": "array",
"items": {
"type": "object",
- "properties" : {
- "id": { "type": "string" },
- "type": { "type": ["string", "null"] },
- "body": { "type": "string" },
- "attachment": { "type": ["string", "null"]},
- "award_emoji": { "type": "array" },
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "body": {
+ "type": "string"
+ },
+ "attachment": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "award_emoji": {
+ "type": "array"
+ },
"author": {
"type": "object",
"properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" },
- "status_tooltip_html": { "type": ["string", "null"] },
- "path": { "type": "string" }
+ "name": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "state": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "status_tooltip_html": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "path": {
+ "type": "string"
+ }
},
"required": [
"id",
@@ -39,43 +80,131 @@
"username"
]
},
- "created_at": { "type": "string", "format": "date-time" },
- "updated_at": { "type": "string", "format": "date-time" },
- "system": { "type": "boolean" },
- "noteable_id": { "type": "integer" },
- "noteable_iid": { "type": ["integer", "null"] },
- "noteable_type": { "type": "string" },
- "resolved": { "type": "boolean" },
- "resolvable": { "type": "boolean" },
- "resolved_by": { "type": ["string", "null"] },
- "resolved_at": { "type": ["string", "null"], "format": "date-time" },
- "note": { "type": "string" },
- "note_html": { "type": "string" },
- "current_user": { "type": "object" },
- "suggestions": { "type": "array" },
- "discussion_id": { "type": "string" },
- "emoji_awardable": { "type": "boolean" },
- "report_abuse_path": { "type": "string" },
- "noteable_note_url": { "type": "string" },
- "resolve_path": { "type": "string" },
- "resolve_with_issue_path": { "type": "string" },
- "cached_markdown_version": { "type": "integer" },
- "human_access": { "type": ["string", "null"] },
- "is_noteable_author": { "type": "boolean" },
- "is_contributor": { "type": "boolean" },
- "project_name": { "type": "string" },
- "toggle_award_path": { "type": "string" },
- "path": { "type": "string" },
- "commands_changes": { "type": "object", "additionalProperties": true },
- "confidential": { "type": ["boolean", "null"] },
- "internal": { "type": ["boolean", "null"] }
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "system": {
+ "type": "boolean"
+ },
+ "noteable_id": {
+ "type": "integer"
+ },
+ "noteable_iid": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "noteable_type": {
+ "type": "string"
+ },
+ "resolved": {
+ "type": "boolean"
+ },
+ "resolvable": {
+ "type": "boolean"
+ },
+ "resolved_by": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "resolved_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "note": {
+ "type": "string"
+ },
+ "note_html": {
+ "type": "string"
+ },
+ "current_user": {
+ "type": "object"
+ },
+ "suggestions": {
+ "type": "array"
+ },
+ "discussion_id": {
+ "type": "string"
+ },
+ "emoji_awardable": {
+ "type": "boolean"
+ },
+ "report_abuse_path": {
+ "type": "string"
+ },
+ "noteable_note_url": {
+ "type": "string"
+ },
+ "resolve_path": {
+ "type": "string"
+ },
+ "resolve_with_issue_path": {
+ "type": "string"
+ },
+ "cached_markdown_version": {
+ "type": "integer"
+ },
+ "human_access": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "is_noteable_author": {
+ "type": "boolean"
+ },
+ "is_contributor": {
+ "type": "boolean"
+ },
+ "project_name": {
+ "type": "string"
+ },
+ "toggle_award_path": {
+ "type": "string"
+ },
+ "path": {
+ "type": "string"
+ },
+ "commands_changes": {
+ "type": "object",
+ "additionalProperties": true
+ },
+ "confidential": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "internal": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ }
},
"required": [
- "id", "attachment", "author", "created_at", "updated_at",
- "system", "noteable_id", "noteable_type"
+ "id",
+ "attachment",
+ "author",
+ "created_at",
+ "updated_at",
+ "system",
+ "noteable_id",
+ "noteable_type"
],
"additionalProperties": false
}
}
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json
index b4a076780d9..08937b5e68b 100644
--- a/spec/fixtures/api/schemas/entities/issue.json
+++ b/spec/fixtures/api/schemas/entities/issue.json
@@ -1,43 +1,142 @@
{
"type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "type": { "type": "string" },
- "author_id": { "type": "integer" },
- "description": { "type": ["string", "null"] },
- "lock_version": { "type": ["integer", "null"] },
- "milestone_id": { "type": ["string", "null"] },
- "title": { "type": "string" },
- "moved_to_id": { "type": ["integer", "null"] },
- "project_id": { "type": "integer" },
- "web_url": { "type": "string" },
- "state": { "type": "string" },
- "create_note_path": { "type": "string" },
- "preview_note_path": { "type": "string" },
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "iid": {
+ "type": "integer"
+ },
+ "type": {
+ "type": "string"
+ },
+ "author_id": {
+ "type": "integer"
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "lock_version": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "milestone_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "title": {
+ "type": "string"
+ },
+ "moved_to_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "project_id": {
+ "type": "integer"
+ },
+ "web_url": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ },
+ "create_note_path": {
+ "type": "string"
+ },
+ "preview_note_path": {
+ "type": "string"
+ },
"current_user": {
"type": "object",
"properties": {
- "can_create_note": { "type": "boolean" },
- "can_update": { "type": "boolean" }
+ "can_create_note": {
+ "type": "boolean"
+ },
+ "can_update": {
+ "type": "boolean"
+ }
}
},
- "created_at": { "type": "date-time" },
- "updated_at": { "type": "date-time" },
- "branch_name": { "type": ["string", "null"] },
- "due_date": { "type": ["string", "null"], "format": "date-time" },
- "confidential": { "type": "boolean" },
- "discussion_locked": { "type": ["boolean", "null"] },
- "updated_by_id": { "type": ["integer", "null"] },
- "time_estimate": { "type": "integer" },
- "total_time_spent": { "type": "integer" },
- "human_time_estimate": { "type": ["integer", "null"] },
- "human_total_time_spent": { "type": ["integer", "null"] },
- "milestone": { "type": ["object", "null"] },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "branch_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "due_date": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "confidential": {
+ "type": "boolean"
+ },
+ "discussion_locked": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "updated_by_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "time_estimate": {
+ "type": "integer"
+ },
+ "total_time_spent": {
+ "type": "integer"
+ },
+ "human_time_estimate": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "human_total_time_spent": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "milestone": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
"labels": {
"type": "array",
- "items": { "$ref": "label.json" }
+ "items": {
+ "$ref": "label.json"
+ }
},
- "assignees": { "type": ["array", "null"] }
+ "assignees": {
+ "type": [
+ "array",
+ "null"
+ ]
+ }
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/member.json b/spec/fixtures/api/schemas/entities/member.json
index 24a4863df9b..cd8a4e0519b 100644
--- a/spec/fixtures/api/schemas/entities/member.json
+++ b/spec/fixtures/api/schemas/entities/member.json
@@ -14,59 +14,130 @@
"is_direct_member"
],
"properties": {
- "id": { "type": "integer" },
- "created_at": { "type": "date-time" },
- "expires_at": { "type": ["date-time", "null"] },
- "requested_at": { "type": ["date-time", "null"] },
- "can_update": { "type": "boolean" },
- "can_remove": { "type": "boolean" },
- "is_direct_member": { "type": "boolean" },
+ "id": {
+ "type": "integer"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "requested_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "can_update": {
+ "type": "boolean"
+ },
+ "can_remove": {
+ "type": "boolean"
+ },
+ "is_direct_member": {
+ "type": "boolean"
+ },
"access_level": {
"type": "object",
- "required": ["integer_value", "string_value"],
+ "required": [
+ "integer_value",
+ "string_value"
+ ],
"properties": {
- "integer_value": { "type": "integer" },
- "string_value": { "type": "string" }
+ "integer_value": {
+ "type": "integer"
+ },
+ "string_value": {
+ "type": "string"
+ }
},
"additionalProperties": false
},
"source": {
"type": "object",
- "required": ["id", "full_name", "web_url"],
+ "required": [
+ "id",
+ "full_name",
+ "web_url"
+ ],
"properties": {
- "id": { "type": "integer" },
- "full_name": { "type": "string" },
- "web_url": { "type": "string" }
+ "id": {
+ "type": "integer"
+ },
+ "full_name": {
+ "type": "string"
+ },
+ "web_url": {
+ "type": "string"
+ }
},
"additionalProperties": false
},
- "valid_roles": { "type": "object" },
- "type": { "type": "string" },
+ "valid_roles": {
+ "type": "object"
+ },
+ "type": {
+ "type": "string"
+ },
"created_by": {
"type": "object",
- "required": ["name", "web_url"],
+ "required": [
+ "name",
+ "web_url"
+ ],
"properties": {
- "name": { "type": "string" },
- "web_url": { "type": "string" }
+ "name": {
+ "type": "string"
+ },
+ "web_url": {
+ "type": "string"
+ }
},
"additionalProperties": false
},
"user": {
"allOf": [
- { "$ref": "member_user_default.json" }
+ {
+ "$ref": "member_user_default.json"
+ }
]
},
- "state": { "type": "integer" },
+ "state": {
+ "type": "integer"
+ },
"invite": {
"type": "object",
- "required": ["email", "avatar_url", "can_resend", "user_state"],
+ "required": [
+ "email",
+ "avatar_url",
+ "can_resend",
+ "user_state"
+ ],
"properties": {
- "email": { "type": "string" },
- "avatar_url": { "type": [ "string", "null" ] },
- "can_resend": { "type": "boolean" },
- "user_state": { "type": "string" }
+ "email": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "can_resend": {
+ "type": "boolean"
+ },
+ "user_state": {
+ "type": "string"
+ }
},
"additionalProperties": false
}
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/merge_request_metrics.json b/spec/fixtures/api/schemas/entities/merge_request_metrics.json
index 3fa767f85df..591c5919b19 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_metrics.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_metrics.json
@@ -1,21 +1,46 @@
{
"type": "object",
- "required": ["closed_at", "merged_at", "closed_by", "merged_by"],
- "properties" : {
- "closed_at": { "type": ["datetime", "null"] },
- "merged_at": { "type": ["datetime", "null"] },
+ "required": [
+ "closed_at",
+ "merged_at",
+ "closed_by",
+ "merged_by"
+ ],
+ "properties": {
+ "closed_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "merged_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
"closed_by": {
"oneOf": [
- { "type": "null" },
- { "$ref": "user.json" }
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "user.json"
+ }
]
},
"merged_by": {
"oneOf": [
- { "type": "null" },
- { "$ref": "user.json" }
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "user.json"
+ }
]
}
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json b/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json
index be2fe19b067..f0509f7a76f 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json
@@ -1,38 +1,128 @@
{
"type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "description": { "type": ["string", "null"] },
- "title": { "type": "string" },
- "auto_merge_strategy": { "type": ["string", "null"] },
- "available_auto_merge_strategies": { "type": "array" },
- "source_branch_protected": { "type": "boolean" },
- "allow_collaboration": { "type": "boolean"},
- "should_be_rebased": { "type": "boolean" },
- "ff_only_enabled": { "type": ["boolean", false] },
- "merge_user": { "type": ["object", "null"] },
- "pipeline": { "type": ["object", "null"] },
- "merge_pipeline": { "type": ["object", "null"] },
- "default_merge_commit_message": { "type": ["string", "null"] },
- "mergeable": { "type": "boolean" },
- "default_merge_commit_message_with_description": { "type": "string" },
- "mergeable_discussions_state": { "type": "boolean" },
- "project_archived": { "type": "boolean" },
- "only_allow_merge_if_pipeline_succeeds": { "type": "boolean" },
- "has_ci": { "type": "boolean" },
- "ci_status": { "type": ["string", "null"] },
- "pipeline_coverage_delta": { "type": ["float", "null"] },
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "iid": {
+ "type": "integer"
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "title": {
+ "type": "string"
+ },
+ "auto_merge_strategy": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "available_auto_merge_strategies": {
+ "type": "array"
+ },
+ "source_branch_protected": {
+ "type": "boolean"
+ },
+ "allow_collaboration": {
+ "type": "boolean"
+ },
+ "should_be_rebased": {
+ "type": "boolean"
+ },
+ "ff_only_enabled": {
+ "type": "boolean"
+ },
+ "merge_user": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "pipeline": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "merge_pipeline": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "default_merge_commit_message": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "mergeable": {
+ "type": "boolean"
+ },
+ "default_merge_commit_message_with_description": {
+ "type": "string"
+ },
+ "mergeable_discussions_state": {
+ "type": "boolean"
+ },
+ "project_archived": {
+ "type": "boolean"
+ },
+ "only_allow_merge_if_pipeline_succeeds": {
+ "type": "boolean"
+ },
+ "has_ci": {
+ "type": "boolean"
+ },
+ "ci_status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "pipeline_coverage_delta": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "float"
+ },
"builds_with_coverage": {
- "type": ["array", "null"],
+ "type": [
+ "array",
+ "null"
+ ],
"items": {
"type": "object",
- "required": ["name", "coverage"]
+ "required": [
+ "name",
+ "coverage"
+ ]
}
},
- "cancel_auto_merge_path": { "type": ["string", "null"] },
- "test_reports_path": { "type": ["string", "null"] },
- "create_issue_to_resolve_discussions_path": { "type": ["string", "null"] },
+ "cancel_auto_merge_path": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "test_reports_path": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "create_issue_to_resolve_discussions_path": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"current_user": {
"type": "object",
"required": [
@@ -42,20 +132,71 @@
"can_create_issue"
],
"properties": {
- "can_remove_source_branch": { "type": "boolean" },
- "can_revert_on_current_merge_request": { "type": ["boolean", "null"] },
- "can_cherry_pick_on_current_merge_request": { "type": ["boolean", "null"] },
- "can_create_issue": { "type": "boolean" }
+ "can_remove_source_branch": {
+ "type": "boolean"
+ },
+ "can_revert_on_current_merge_request": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "can_cherry_pick_on_current_merge_request": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "can_create_issue": {
+ "type": "boolean"
+ }
},
"additionalProperties": false
},
- "can_push_to_source_branch": { "type": "boolean" },
- "new_blob_path": { "type": ["string", "null"] },
- "rebase_path": { "type": ["string", "null"] },
- "conflict_resolution_path": { "type": ["string", "null"] },
- "remove_wip_path": { "type": ["string", "null"] },
- "merge_path": { "type": ["string", "null"] },
- "cherry_pick_in_fork_path": { "type": ["string", "null"] },
- "revert_in_fork_path": { "type": ["string", "null"] }
+ "can_push_to_source_branch": {
+ "type": "boolean"
+ },
+ "new_blob_path": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "rebase_path": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "conflict_resolution_path": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "remove_wip_path": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "merge_path": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "cherry_pick_in_fork_path": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "revert_in_fork_path": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/test_case.json b/spec/fixtures/api/schemas/entities/test_case.json
index 483fa881f8c..660d40c4a9f 100644
--- a/spec/fixtures/api/schemas/entities/test_case.json
+++ b/spec/fixtures/api/schemas/entities/test_case.json
@@ -1,24 +1,57 @@
{
"type": "object",
- "required" : [
+ "required": [
"status",
"name"
],
"properties": {
- "status": { "type": "string" },
- "name": { "type": "string" },
- "classname": { "type": "string" },
- "file": { "type": ["string", "null"] },
- "execution_time": { "type": "float" },
- "system_output": { "type": ["string", "null"] },
- "stack_trace": { "type": ["string", "null"] },
- "attachment_url": { "type": ["string", "null"] },
+ "status": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "classname": {
+ "type": "string"
+ },
+ "file": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "execution_time": {
+ "type": "number",
+ "format": "float"
+ },
+ "system_output": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "stack_trace": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "attachment_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"recent_failures": {
"oneOf": [
- { "type": "null" },
- { "$ref": "test_case/recent_failures.json" }
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "test_case/recent_failures.json"
+ }
]
}
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/trigger.json b/spec/fixtures/api/schemas/entities/trigger.json
index 5c46142673f..c6ccd84dcdd 100644
--- a/spec/fixtures/api/schemas/entities/trigger.json
+++ b/spec/fixtures/api/schemas/entities/trigger.json
@@ -10,14 +10,21 @@
],
"properties": {
"description": {
- "type": ["string", "null"]
+ "type": [
+ "string",
+ "null"
+ ]
},
"owner": {
"type": "object",
"$ref": "user.json"
},
"last_used": {
- "type": ["datetime", "null"]
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
},
"token": {
"type": "string"
@@ -36,4 +43,4 @@
}
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/environment.json b/spec/fixtures/api/schemas/environment.json
index 87b6e5da370..fe842844223 100644
--- a/spec/fixtures/api/schemas/environment.json
+++ b/spec/fixtures/api/schemas/environment.json
@@ -15,52 +15,120 @@
"can_stop"
],
"properties": {
- "id": { "type": "integer" },
- "global_id": { "type": "string" },
- "name": { "type": "string" },
- "state": { "type": "string" },
- "external_url": { "$ref": "types/nullable_string.json" },
- "environment_type": { "$ref": "types/nullable_string.json" },
- "name_without_type": { "type": "string" },
- "has_stop_action": { "type": "boolean" },
- "environment_path": { "type": "string" },
- "stop_path": { "type": "string" },
- "cancel_auto_stop_path": { "type": "string" },
- "folder_path": { "type": "string" },
- "logs_path": { "type": "string" },
- "logs_api_path": { "type": "string" },
- "enable_advanced_logs_querying": { "type": "boolean" },
- "created_at": { "type": "string", "format": "date-time" },
- "updated_at": { "type": "string", "format": "date-time" },
- "auto_stop_at": { "type": "string", "format": "date-time" },
- "can_stop": { "type": "boolean" },
- "has_opened_alert": { "type": "boolean" },
- "tier": { "type": "string" },
- "required_approval_count": { "type": "integer" },
- "cluster_type": { "type": "types/nullable_string.json" },
- "terminal_path": { "type": "types/nullable_string.json" },
+ "id": {
+ "type": "integer"
+ },
+ "global_id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ },
+ "external_url": {
+ "$ref": "types/nullable_string.json",
+ "format": "uri"
+ },
+ "environment_type": {
+ "$ref": "types/nullable_string.json",
+ "format": "uri"
+ },
+ "name_without_type": {
+ "type": "string"
+ },
+ "has_stop_action": {
+ "type": "boolean"
+ },
+ "environment_path": {
+ "type": "string"
+ },
+ "stop_path": {
+ "type": "string"
+ },
+ "cancel_auto_stop_path": {
+ "type": "string"
+ },
+ "folder_path": {
+ "type": "string"
+ },
+ "logs_path": {
+ "type": "string"
+ },
+ "logs_api_path": {
+ "type": "string"
+ },
+ "enable_advanced_logs_querying": {
+ "type": "boolean"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "auto_stop_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "can_stop": {
+ "type": "boolean"
+ },
+ "has_opened_alert": {
+ "type": "boolean"
+ },
+ "tier": {
+ "type": "string"
+ },
+ "required_approval_count": {
+ "type": "integer"
+ },
+ "cluster_type": {
+ "ref": "types/nullable_string.json"
+ },
+ "terminal_path": {
+ "ref": "types/nullable_string.json"
+ },
"rollout_status": {
"oneOf": [
- { "type": "null" },
- { "$ref": "rollout_status.json" }
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "rollout_status.json"
+ }
]
},
"last_deployment": {
"oneOf": [
- { "type": "null" },
- { "$ref": "deployment.json" },
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "deployment.json"
+ },
{
"type": "object",
- "properties" : {
- "name": { "type": "string" },
- "build_path": { "type": "string" }
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "build_path": {
+ "type": "string"
+ }
}
}
]
},
- "can_delete": { "type": "boolean" }
- ,
- "delete_path": { "type": "string" }
+ "can_delete": {
+ "type": "boolean"
+ },
+ "delete_path": {
+ "type": "string"
+ }
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/group_link/group_link.json b/spec/fixtures/api/schemas/group_link/group_link.json
index 3c2195df11e..885ed6d18e0 100644
--- a/spec/fixtures/api/schemas/group_link/group_link.json
+++ b/spec/fixtures/api/schemas/group_link/group_link.json
@@ -11,34 +11,82 @@
"is_direct_member"
],
"properties": {
- "id": { "type": "integer" },
- "created_at": { "type": "date-time" },
- "expires_at": { "type": ["date-time", "null"] },
+ "id": {
+ "type": "integer"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
"access_level": {
"type": "object",
- "required": ["integer_value", "string_value"],
+ "required": [
+ "integer_value",
+ "string_value"
+ ],
"properties": {
- "integer_value": { "type": "integer" },
- "string_value": { "type": "string" }
+ "integer_value": {
+ "type": "integer"
+ },
+ "string_value": {
+ "type": "string"
+ }
},
"additionalProperties": false
},
- "valid_roles": { "type": "object" },
+ "valid_roles": {
+ "type": "object"
+ },
"shared_with_group": {
"type": "object",
- "required": ["id", "name", "full_name", "full_path", "avatar_url", "web_url"],
+ "required": [
+ "id",
+ "name",
+ "full_name",
+ "full_path",
+ "avatar_url",
+ "web_url"
+ ],
"properties": {
- "id": { "type": "integer" },
- "name": { "type": "string" },
- "full_name": { "type": "string" },
- "full_path": { "type": "string" },
- "avatar_url": { "type": ["string", "null"] },
- "web_url": { "type": "string" }
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "full_name": {
+ "type": "string"
+ },
+ "full_path": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "web_url": {
+ "type": "string"
+ }
},
"additionalProperties": false
},
- "can_update": { "type": "boolean" },
- "can_remove": { "type": "boolean" },
- "is_direct_member": { "type": "boolean" }
+ "can_update": {
+ "type": "boolean"
+ },
+ "can_remove": {
+ "type": "boolean"
+ },
+ "is_direct_member": {
+ "type": "boolean"
+ }
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index aefba89d9e2..ac8c05eb377 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -1,43 +1,104 @@
{
"type": "object",
- "required" : [
+ "required": [
"iid",
"title",
"confidential"
],
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": ["integer", "null"] },
- "title": { "type": "string" },
- "confidential": { "type": "boolean" },
- "due_date": { "type": ["string", "null"] },
- "relative_position": { "type": ["integer", "null"] },
- "time_estimate": { "type": "integer" },
- "type": { "type": "string", "enum": ["ISSUE", "INCIDENT", "TEST_CASE", "REQUIREMENT"] },
- "issue_sidebar_endpoint": { "type": "string" },
- "toggle_subscription_endpoint": { "type": "string" },
- "assignable_labels_endpoint": { "type": "string" },
- "reference_path": { "type": "string" },
- "real_path": { "type": "string" },
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "iid": {
+ "type": "integer"
+ },
+ "project_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "title": {
+ "type": "string"
+ },
+ "confidential": {
+ "type": "boolean"
+ },
+ "due_date": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "relative_position": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "time_estimate": {
+ "type": "integer"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "ISSUE",
+ "INCIDENT",
+ "TEST_CASE",
+ "REQUIREMENT"
+ ]
+ },
+ "issue_sidebar_endpoint": {
+ "type": "string"
+ },
+ "toggle_subscription_endpoint": {
+ "type": "string"
+ },
+ "assignable_labels_endpoint": {
+ "type": "string"
+ },
+ "reference_path": {
+ "type": "string"
+ },
+ "real_path": {
+ "type": "string"
+ },
"project": {
- "id": { "type": "integer" },
- "path": { "type": "string" }
+ "id": {
+ "type": "integer"
+ },
+ "path": {
+ "type": "string"
+ }
},
"labels": {
"type": "array",
- "items": { "$ref": "entities/label.json" }
+ "items": {
+ "$ref": "entities/label.json"
+ }
},
"assignee": {
- "id": { "type": "integer" },
- "name": { "type": "string" },
- "username": { "type": "string" },
- "avatar_url": { "type": "uri" }
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": "string",
+ "format": "uri"
+ }
},
"assignees": {
"type": "array",
"items": {
- "type": ["object", "null"],
+ "type": [
+ "object",
+ "null"
+ ],
"required": [
"id",
"name",
@@ -45,13 +106,27 @@
"avatar_url"
],
"properties": {
- "id": { "type": "integer" },
- "name": { "type": "string" },
- "username": { "type": "string" },
- "avatar_url": { "type": "uri" }
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": "string",
+ "format": "uri"
+ }
}
}
},
- "subscribed": { "type": ["boolean", "null"] }
+ "subscribed": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ }
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/jira_connect/author.json b/spec/fixtures/api/schemas/jira_connect/author.json
index bd2cff96d99..e653db9a97d 100644
--- a/spec/fixtures/api/schemas/jira_connect/author.json
+++ b/spec/fixtures/api/schemas/jira_connect/author.json
@@ -1,12 +1,27 @@
{
"type": "object",
"properties": {
- "name": { "type": "string" },
- "email": { "type": "string" },
- "username": { "type": "string" },
- "url": { "type": "uri" },
- "avatar": { "type": "uri" }
+ "name": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "avatar": {
+ "type": "string",
+ "format": "uri"
+ }
},
- "required": [ "name", "email" ],
+ "required": [
+ "name",
+ "email"
+ ],
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/jira_connect/branch.json b/spec/fixtures/api/schemas/jira_connect/branch.json
index c397d88fa91..237dc8abbc0 100644
--- a/spec/fixtures/api/schemas/jira_connect/branch.json
+++ b/spec/fixtures/api/schemas/jira_connect/branch.json
@@ -1,19 +1,38 @@
{
"type": "object",
"properties": {
- "id": { "type": "string" },
- "issueKeys": { "type": "array" },
- "name": { "type": "string" },
+ "id": {
+ "type": "string"
+ },
+ "issueKeys": {
+ "type": "array"
+ },
+ "name": {
+ "type": "string"
+ },
"lastCommit": {
"$ref": "./commit.json"
},
- "url": { "type": "uri" },
- "createPullRequestUrl": { "type": "uri" },
- "updateSequenceId": { "type": "integer" }
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "createPullRequestUrl": {
+ "type": "string",
+ "format": "uri"
+ },
+ "updateSequenceId": {
+ "type": "integer"
+ }
},
"required": [
- "id", "issueKeys", "name", "lastCommit",
- "url", "createPullRequestUrl", "updateSequenceId"
+ "id",
+ "issueKeys",
+ "name",
+ "lastCommit",
+ "url",
+ "createPullRequestUrl",
+ "updateSequenceId"
],
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/jira_connect/commit.json b/spec/fixtures/api/schemas/jira_connect/commit.json
index 794cf9ef365..0c35d650559 100644
--- a/spec/fixtures/api/schemas/jira_connect/commit.json
+++ b/spec/fixtures/api/schemas/jira_connect/commit.json
@@ -1,29 +1,61 @@
{
"type": "object",
"properties": {
- "id": { "type": "string" },
- "issueKeys": { "type": "array" },
- "hash": { "type": "string" },
- "displayId": { "type": "string" },
- "message": { "type": "string" },
- "flags": { "type": "array" },
+ "id": {
+ "type": "string"
+ },
+ "issueKeys": {
+ "type": "array"
+ },
+ "hash": {
+ "type": "string"
+ },
+ "displayId": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "flags": {
+ "type": "array"
+ },
"author": {
"$ref": "./author.json"
},
- "fileCount": { "type": "integer" },
+ "fileCount": {
+ "type": "integer"
+ },
"files": {
"type": "array",
"items": {
"$ref": "./file.json"
}
},
- "authorTimestamp": { "type": "timestamp" },
- "url": { "type": "uri" },
- "updateSequenceId": { "type": "integer" }
+ "authorTimestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "updateSequenceId": {
+ "type": "integer"
+ }
},
"required": [
- "id", "issueKeys", "hash", "displayId", "message", "flags", "author",
- "fileCount", "files", "authorTimestamp", "url", "updateSequenceId"
+ "id",
+ "issueKeys",
+ "hash",
+ "displayId",
+ "message",
+ "flags",
+ "author",
+ "fileCount",
+ "files",
+ "authorTimestamp",
+ "url",
+ "updateSequenceId"
],
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/jira_connect/file.json b/spec/fixtures/api/schemas/jira_connect/file.json
index 34718991237..3e93911ece4 100644
--- a/spec/fixtures/api/schemas/jira_connect/file.json
+++ b/spec/fixtures/api/schemas/jira_connect/file.json
@@ -1,14 +1,29 @@
{
"type": "object",
"properties": {
- "path": { "type": "string" },
- "changeType": { "type": "string" },
- "linesAdded": { "type": "integer" },
- "linesRemoved": { "type": "integer" },
- "url": { "type": "uri" }
+ "path": {
+ "type": "string"
+ },
+ "changeType": {
+ "type": "string"
+ },
+ "linesAdded": {
+ "type": "integer"
+ },
+ "linesRemoved": {
+ "type": "integer"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ }
},
"required": [
- "path", "changeType", "linesAdded", "linesRemoved", "url"
+ "path",
+ "changeType",
+ "linesAdded",
+ "linesRemoved",
+ "url"
],
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/jira_connect/pull_request.json b/spec/fixtures/api/schemas/jira_connect/pull_request.json
index 56ce6faf498..430752335be 100644
--- a/spec/fixtures/api/schemas/jira_connect/pull_request.json
+++ b/spec/fixtures/api/schemas/jira_connect/pull_request.json
@@ -1,26 +1,63 @@
{
"type": "object",
"properties": {
- "id": { "type": "string" },
- "issueKeys": { "type": "array" },
- "displayId": { "type": "string" },
- "title": { "type": "string" },
+ "id": {
+ "type": "string"
+ },
+ "issueKeys": {
+ "type": "array"
+ },
+ "displayId": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
"author": {
"$ref": "./author.json"
},
- "commentCount": { "type": "integer" },
- "sourceBranch": { "type": "string" },
- "destinationBranch": { "type": "string" },
- "lastUpdate": { "type": "timestamp" },
- "status": { "type": "string" },
- "sourceBranchUrl": { "type": "uri" },
- "url": { "type": "uri" },
- "updateSequenceId": { "type": "integer" }
+ "commentCount": {
+ "type": "integer"
+ },
+ "sourceBranch": {
+ "type": "string"
+ },
+ "destinationBranch": {
+ "type": "string"
+ },
+ "lastUpdate": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "status": {
+ "type": "string"
+ },
+ "sourceBranchUrl": {
+ "type": "string",
+ "format": "uri"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "updateSequenceId": {
+ "type": "integer"
+ }
},
"required": [
- "id", "issueKeys", "displayId", "title", "author", "commentCount",
- "sourceBranch", "destinationBranch", "lastUpdate", "status",
- "sourceBranchUrl", "url", "updateSequenceId"
+ "id",
+ "issueKeys",
+ "displayId",
+ "title",
+ "author",
+ "commentCount",
+ "sourceBranch",
+ "destinationBranch",
+ "lastUpdate",
+ "status",
+ "sourceBranchUrl",
+ "url",
+ "updateSequenceId"
],
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/jira_connect/repository.json b/spec/fixtures/api/schemas/jira_connect/repository.json
index 9e81d77bc6a..5a45bf7cc77 100644
--- a/spec/fixtures/api/schemas/jira_connect/repository.json
+++ b/spec/fixtures/api/schemas/jira_connect/repository.json
@@ -1,11 +1,29 @@
{
"type": "object",
"properties": {
- "id": { "type": "string" },
- "name": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "url": { "type": "uri" },
- "avatar": { "type": "uri" },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "avatar": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "uri"
+ },
"commits": {
"type": "array",
"items": {
@@ -24,11 +42,20 @@
"$ref": "./pull_request.json"
}
},
- "updateSequenceId": { "type": "integer" }
+ "updateSequenceId": {
+ "type": "integer"
+ }
},
"required": [
- "id", "name", "description", "url", "avatar",
- "commits", "branches", "pullRequests", "updateSequenceId"
+ "id",
+ "name",
+ "description",
+ "url",
+ "avatar",
+ "commits",
+ "branches",
+ "pullRequests",
+ "updateSequenceId"
],
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/job/build_trace_line.json b/spec/fixtures/api/schemas/job/build_trace_line.json
index 18726dff2bb..ada2b4edb7d 100644
--- a/spec/fixtures/api/schemas/job/build_trace_line.json
+++ b/spec/fixtures/api/schemas/job/build_trace_line.json
@@ -6,13 +6,23 @@
"content"
],
"properties": {
- "offset": { "type": "integer" },
+ "offset": {
+ "type": "integer"
+ },
"content": {
"type": "array",
- "items": { "$ref": "./build_trace_line_content.json" }
+ "items": {
+ "$ref": "./build_trace_line_content.json"
+ }
+ },
+ "section": {
+ "type": "string"
+ },
+ "section_header": {
+ "type": "boolean"
},
- "section": "string",
- "section_header": "boolean",
- "section_duration": "string"
+ "section_duration": {
+ "type": "string"
+ }
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/merge_request.json b/spec/fixtures/api/schemas/merge_request.json
index 36962660cd9..416391e454f 100644
--- a/spec/fixtures/api/schemas/merge_request.json
+++ b/spec/fixtures/api/schemas/merge_request.json
@@ -1,12 +1,17 @@
{
"type": "object",
- "required" : [
+ "required": [
"iid",
"url"
],
- "properties" : {
- "iid": { "type": "integer" },
- "url": { "type": "uri" }
+ "properties": {
+ "iid": {
+ "type": "integer"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ }
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/ml/get_experiment.json b/spec/fixtures/api/schemas/ml/get_experiment.json
index 482455a89e1..13630ec92e9 100644
--- a/spec/fixtures/api/schemas/ml/get_experiment.json
+++ b/spec/fixtures/api/schemas/ml/get_experiment.json
@@ -23,11 +23,28 @@
"type": "string"
},
"lifecycle_stage": {
- "type": {
- "enum": [
- "active",
- "deleted"
- ]
+ "type": "string",
+ "enum": [
+ "active",
+ "deleted"
+ ]
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "key",
+ "value"
+ ],
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
}
}
}
diff --git a/spec/fixtures/api/schemas/ml/list_experiments.json b/spec/fixtures/api/schemas/ml/list_experiments.json
index 4c3e834abc6..10f512cd1eb 100644
--- a/spec/fixtures/api/schemas/ml/list_experiments.json
+++ b/spec/fixtures/api/schemas/ml/list_experiments.json
@@ -25,12 +25,11 @@
"type": "string"
},
"lifecycle_stage": {
- "type": {
- "enum": [
- "active",
- "deleted"
- ]
- }
+ "type": "string",
+ "enum": [
+ "active",
+ "deleted"
+ ]
}
}
}
diff --git a/spec/fixtures/api/schemas/ml/run.json b/spec/fixtures/api/schemas/ml/run.json
index 48d0ed25ce4..5591eee0800 100644
--- a/spec/fixtures/api/schemas/ml/run.json
+++ b/spec/fixtures/api/schemas/ml/run.json
@@ -45,24 +45,24 @@
"end_time": {
"type": "integer"
},
- "user_id": "",
+ "user_id": {
+ "type": "string"
+ },
"status": {
- "type": {
- "enum": [
- "RUNNING",
- "SCHEDULED",
- "FINISHED",
- "FAILED",
- "KILLED"
- ]
- }
+ "type": "string",
+ "enum": [
+ "RUNNING",
+ "SCHEDULED",
+ "FINISHED",
+ "FAILED",
+ "KILLED"
+ ]
},
"lifecycle_stage": {
- "type": {
- "enum": [
- "active"
- ]
- }
+ "type": "string",
+ "enum": [
+ "active"
+ ]
}
}
},
diff --git a/spec/fixtures/api/schemas/ml/update_run.json b/spec/fixtures/api/schemas/ml/update_run.json
index b429444120f..fb1291a9bb9 100644
--- a/spec/fixtures/api/schemas/ml/update_run.json
+++ b/spec/fixtures/api/schemas/ml/update_run.json
@@ -20,16 +20,44 @@
"end_time"
],
"properties": {
- "run_id": { "type": "string" },
- "run_uuid": { "type": "string" },
- "experiment_id": { "type": "string" },
- "artifact_location": { "type": "string" },
- "start_time": { "type": "integer" },
- "end_time": { "type": "integer" },
- "user_id": { "type": "string" },
- "status": { "type": { "enum" : ["RUNNING", "SCHEDULED", "FINISHED", "FAILED", "KILLED"] } },
- "lifecycle_stage": { "type": { "enum" : ["active"] } }
+ "run_id": {
+ "type": "string"
+ },
+ "run_uuid": {
+ "type": "string"
+ },
+ "experiment_id": {
+ "type": "string"
+ },
+ "artifact_location": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "integer"
+ },
+ "end_time": {
+ "type": "integer"
+ },
+ "user_id": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string",
+ "enum": [
+ "RUNNING",
+ "SCHEDULED",
+ "FINISHED",
+ "FAILED",
+ "KILLED"
+ ]
+ },
+ "lifecycle_stage": {
+ "type": "string",
+ "enum": [
+ "active"
+ ]
+ }
}
}
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json
index ef5942b7eb3..ade5a76d934 100644
--- a/spec/fixtures/api/schemas/pipeline_schedule.json
+++ b/spec/fixtures/api/schemas/pipeline_schedule.json
@@ -1,53 +1,142 @@
{
"type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "description": { "type": "string" },
- "ref": { "type": "string" },
- "cron": { "type": "string" },
- "cron_timezone": { "type": "string" },
- "next_run_at": { "type": "string" },
- "active": { "type": "boolean" },
- "created_at": { "type": ["string", "null"], "format": "date-time" },
- "updated_at": { "type": ["string", "null"], "format": "date-time" },
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "description": {
+ "type": "string"
+ },
+ "ref": {
+ "type": "string"
+ },
+ "cron": {
+ "type": "string"
+ },
+ "cron_timezone": {
+ "type": "string"
+ },
+ "next_run_at": {
+ "type": "string"
+ },
+ "active": {
+ "type": "boolean"
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
"last_pipeline": {
- "type": ["object", "null"],
+ "type": [
+ "object",
+ "null"
+ ],
"properties": {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": "integer" },
- "sha": { "type": "string" },
- "ref": { "type": "string" },
- "status": { "type": "string" },
- "source": { "type": "string" },
- "web_url": { "type": ["string", "null"] },
- "created_at": { "type": ["string", "null"], "format": "date-time" },
- "updated_at": { "type": ["string", "null"], "format": "date-time" }
+ "id": {
+ "type": "integer"
+ },
+ "iid": {
+ "type": "integer"
+ },
+ "project_id": {
+ "type": "integer"
+ },
+ "sha": {
+ "type": "string"
+ },
+ "ref": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ },
+ "web_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
},
"additionalProperties": false
},
"owner": {
"type": "object",
"properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
+ "name": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "state": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ }
},
"required": [
- "id", "name", "username", "state", "avatar_url", "web_url"
+ "id",
+ "name",
+ "username",
+ "state",
+ "avatar_url",
+ "web_url"
]
},
"variables": {
"type": "array",
- "items": { "$ref": "pipeline_schedule_variable.json" }
+ "items": {
+ "$ref": "pipeline_schedule_variable.json"
+ }
}
},
"required": [
- "id", "description", "ref", "cron", "cron_timezone", "next_run_at",
- "active", "created_at", "updated_at", "owner"
+ "id",
+ "description",
+ "ref",
+ "cron",
+ "cron_timezone",
+ "next_run_at",
+ "active",
+ "created_at",
+ "updated_at",
+ "owner"
],
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json b/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json
index 47b5d283b8c..f32ae3d334c 100644
--- a/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json
+++ b/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json
@@ -1,13 +1,29 @@
{
+ "type": "array",
"items": {
+ "type": "object",
+ "required": [
+ "group",
+ "metrics",
+ "priority"
+ ],
"properties": {
"group": {
"type": "string"
},
"metrics": {
+ "type": "array",
"items": {
+ "type": "object",
+ "required": [
+ "queries",
+ "title",
+ "weight"
+ ],
"properties": {
"queries": {
+ "type": "array",
+ "required": [],
"items": {
"properties": {
"query_range": {
@@ -16,13 +32,33 @@
"query": {
"type": "string"
},
+ "label": {
+ "type": "string"
+ },
+ "unit": {
+ "type": "string"
+ },
"result": {
- "type": "any"
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "metric",
+ "values"
+ ],
+ "properties": {
+ "metric": {
+ "type": "object"
+ },
+ "values": {
+ "type": "array"
+ }
+ }
+ }
}
},
"type": "object"
- },
- "type": "array"
+ }
},
"title": {
"type": "string"
@@ -33,26 +69,12 @@
"y_label": {
"type": "string"
}
- },
- "type": "object"
- },
- "required": [
- "metrics",
- "title",
- "weight"
- ],
- "type": "array"
+ }
+ }
},
"priority": {
"type": "integer"
}
- },
- "type": "object"
- },
- "required": [
- "group",
- "priority",
- "metrics"
- ],
- "type": "array"
+ }
+ }
} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/basic_environment.json b/spec/fixtures/api/schemas/public_api/v4/basic_environment.json
new file mode 100644
index 00000000000..f22f73f573b
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/basic_environment.json
@@ -0,0 +1,34 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "slug",
+ "external_url",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "slug": {
+ "type": "string"
+ },
+ "external_url": {
+ "$ref": "../../types/nullable_string.json"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "additionalProperties": false
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/basic_environments.json b/spec/fixtures/api/schemas/public_api/v4/basic_environments.json
new file mode 100644
index 00000000000..6620e423c96
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/basic_environments.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "$ref": "./basic_environment.json"
+ }
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/branch.json b/spec/fixtures/api/schemas/public_api/v4/branch.json
index 0073a6d89fc..ed65f3c2e5c 100644
--- a/spec/fixtures/api/schemas/public_api/v4/branch.json
+++ b/spec/fixtures/api/schemas/public_api/v4/branch.json
@@ -1,6 +1,6 @@
{
"type": "object",
- "required" : [
+ "required": [
"name",
"commit",
"merged",
@@ -10,16 +10,35 @@
"developers_can_merge",
"web_url"
],
- "properties" : {
- "name": { "type": "string" },
- "commit": { "$ref": "commit/basic.json" },
- "merged": { "type": "boolean" },
- "protected": { "type": "boolean" },
- "default": { "type": "boolean" },
- "developers_can_push": { "type": "boolean" },
- "developers_can_merge": { "type": "boolean" },
- "can_push": { "type": "boolean" },
- "web_url": { "type": "uri" }
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "commit": {
+ "$ref": "commit/basic.json"
+ },
+ "merged": {
+ "type": "boolean"
+ },
+ "protected": {
+ "type": "boolean"
+ },
+ "default": {
+ "type": "boolean"
+ },
+ "developers_can_push": {
+ "type": "boolean"
+ },
+ "developers_can_merge": {
+ "type": "boolean"
+ },
+ "can_push": {
+ "type": "boolean"
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ }
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/deploy_key.json b/spec/fixtures/api/schemas/public_api/v4/deploy_key.json
index 99e57a4c218..4f8b5c8422e 100644
--- a/spec/fixtures/api/schemas/public_api/v4/deploy_key.json
+++ b/spec/fixtures/api/schemas/public_api/v4/deploy_key.json
@@ -7,20 +7,50 @@
"expires_at",
"key",
"fingerprint_sha256",
+ "usage_type",
"projects_with_write_access"
],
"properties": {
- "id": { "type": "integer" },
- "title": { "type": "string" },
- "created_at": { "type": "string", "format": "date-time" },
- "expires_at": { "type": ["string", "null"], "format": "date-time" },
- "key": { "type": "string" },
- "fingerprint": { "type": "string" },
- "fingerprint_sha256": { "type": "string" },
+ "id": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "key": {
+ "type": "string"
+ },
+ "fingerprint": {
+ "type": "string"
+ },
+ "fingerprint_sha256": {
+ "type": "string"
+ },
+ "usage_type": {
+ "type": "string",
+ "enum": [
+ "auth",
+ "signing",
+ "auth_and_signing"
+ ]
+ },
"projects_with_write_access": {
"type": "array",
- "items": { "$ref": "project/identity.json" }
+ "items": {
+ "$ref": "project/identity.json"
+ }
}
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/deploy_token.json b/spec/fixtures/api/schemas/public_api/v4/deploy_token.json
index 102ab95a4ee..664740c2a3c 100644
--- a/spec/fixtures/api/schemas/public_api/v4/deploy_token.json
+++ b/spec/fixtures/api/schemas/public_api/v4/deploy_token.json
@@ -19,7 +19,9 @@
"username": {
"type": "string"
},
- "expires_at": { "type": "string" },
+ "expires_at": {
+ "type": "string"
+ },
"scopes": {
"type": "array",
"items": {
diff --git a/spec/fixtures/api/schemas/public_api/v4/environments.json b/spec/fixtures/api/schemas/public_api/v4/environments.json
index f739c06f604..1697da0f231 100644
--- a/spec/fixtures/api/schemas/public_api/v4/environments.json
+++ b/spec/fixtures/api/schemas/public_api/v4/environments.json
@@ -1,9 +1,6 @@
{
"type": "array",
"items": {
- "type": "object",
- "properties": {
- "$ref": "./environment.json"
- }
+ "$ref": "./environment.json"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/feature_flag_scopes.json b/spec/fixtures/api/schemas/public_api/v4/feature_flag_scopes.json
index b1a7021db8b..1df46780a03 100644
--- a/spec/fixtures/api/schemas/public_api/v4/feature_flag_scopes.json
+++ b/spec/fixtures/api/schemas/public_api/v4/feature_flag_scopes.json
@@ -1,9 +1,6 @@
{
"type": "array",
"items": {
- "type": "object",
- "properties": {
- "$ref": "./feature_flag_scope.json"
- }
+ "$ref": "./feature_flag_scope.json"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/feature_flags.json b/spec/fixtures/api/schemas/public_api/v4/feature_flags.json
index c19df0443d9..f381adc3c8b 100644
--- a/spec/fixtures/api/schemas/public_api/v4/feature_flags.json
+++ b/spec/fixtures/api/schemas/public_api/v4/feature_flags.json
@@ -1,9 +1,6 @@
{
"type": "array",
"items": {
- "type": "object",
- "properties": {
- "$ref": "./feature_flag.json"
- }
+ "$ref": "./feature_flag.json"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/integration.json b/spec/fixtures/api/schemas/public_api/v4/integration.json
index b6f13d1cfe7..d1538db7de4 100644
--- a/spec/fixtures/api/schemas/public_api/v4/integration.json
+++ b/spec/fixtures/api/schemas/public_api/v4/integration.json
@@ -1,24 +1,62 @@
{
"type": "object",
"properties": {
- "id": { "type": "integer" },
- "title": { "type": "string" },
- "slug": { "type": "string" },
- "created_at": { "type": "date-time" },
- "updated_at": { "type": "date-time" },
- "active": { "type": "boolean" },
- "commit_events": { "type": "boolean" },
- "push_events": { "type": "boolean" },
- "issues_events": { "type": "boolean" },
- "confidential_issues_events": { "type": "boolean" },
- "merge_requests_events": { "type": "boolean" },
- "tag_push_events": { "type": "boolean" },
- "note_events": { "type": "boolean" },
- "confidential_note_events": { "type": "boolean" },
- "pipeline_events": { "type": "boolean" },
- "wiki_page_events": { "type": "boolean" },
- "job_events": { "type": "boolean" },
- "comment_on_event_enabled": { "type": "boolean" }
+ "id": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "slug": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "active": {
+ "type": "boolean"
+ },
+ "commit_events": {
+ "type": "boolean"
+ },
+ "push_events": {
+ "type": "boolean"
+ },
+ "issues_events": {
+ "type": "boolean"
+ },
+ "confidential_issues_events": {
+ "type": "boolean"
+ },
+ "merge_requests_events": {
+ "type": "boolean"
+ },
+ "tag_push_events": {
+ "type": "boolean"
+ },
+ "note_events": {
+ "type": "boolean"
+ },
+ "confidential_note_events": {
+ "type": "boolean"
+ },
+ "pipeline_events": {
+ "type": "boolean"
+ },
+ "wiki_page_events": {
+ "type": "boolean"
+ },
+ "job_events": {
+ "type": "boolean"
+ },
+ "comment_on_event_enabled": {
+ "type": "boolean"
+ }
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/issue.json b/spec/fixtures/api/schemas/public_api/v4/issue.json
index 90b368b5226..c2b096a922f 100644
--- a/spec/fixtures/api/schemas/public_api/v4/issue.json
+++ b/spec/fixtures/api/schemas/public_api/v4/issue.json
@@ -1,16 +1,47 @@
{
"type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": "integer" },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "discussion_locked": { "type": ["boolean", "null"] },
- "closed_at": { "type": ["string", "null"] },
- "created_at": { "type": "string", "format": "date-time" },
- "updated_at": { "type": "string", "format": "date-time" },
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "iid": {
+ "type": "integer"
+ },
+ "project_id": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "state": {
+ "type": "string"
+ },
+ "discussion_locked": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "closed_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
"labels": {
"type": "array",
"items": {
@@ -18,58 +49,171 @@
}
},
"milestone": {
- "type": ["object", "null"],
+ "type": [
+ "object",
+ "null"
+ ],
"properties": {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": ["integer", "null"] },
- "group_id": { "type": ["integer", "null"] },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "created_at": { "type": "string", "format": "date-time" },
- "updated_at": { "type": "string", "format": "date-time" },
- "due_date": { "type": "string" , "format": "date-time" },
- "start_date": { "type": "string", "format": "date-time" }
+ "id": {
+ "type": "integer"
+ },
+ "iid": {
+ "type": "integer"
+ },
+ "project_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "group_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "state": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "due_date": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "anyOf": [
+ {
+ "format": "date-time"
+ },
+ {
+ "format": "date"
+ }
+ ]
+ },
+ "start_date": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "anyOf": [
+ {
+ "format": "date-time"
+ },
+ {
+ "format": "date"
+ }
+ ]
+ },
+ "expired": {
+ "type": "boolean"
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ }
},
"additionalProperties": false
},
"assignees": {
"type": "array",
"items": {
- "type": ["object", "null"],
+ "type": [
+ "object",
+ "null"
+ ],
"properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
+ "name": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "state": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ }
},
"additionalProperties": false
}
},
"assignee": {
- "type": ["object", "null"],
+ "type": [
+ "object",
+ "null"
+ ],
"properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
+ "name": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "state": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ }
},
"additionalProperties": false
},
"author": {
"type": "object",
"properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
+ "name": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "state": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ }
},
"required": [
"id",
@@ -80,30 +224,88 @@
"web_url"
]
},
- "user_notes_count": { "type": "integer" },
- "upvotes": { "type": "integer" },
- "downvotes": { "type": "integer" },
- "due_date": { "type": ["string", "null"] },
- "confidential": { "type": "boolean" },
- "web_url": { "type": "uri" },
- "severity": { "type": "string", "enum": ["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] },
+ "user_notes_count": {
+ "type": "integer"
+ },
+ "upvotes": {
+ "type": "integer"
+ },
+ "downvotes": {
+ "type": "integer"
+ },
+ "due_date": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "confidential": {
+ "type": "boolean"
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "severity": {
+ "type": "string",
+ "enum": [
+ "UNKNOWN",
+ "LOW",
+ "MEDIUM",
+ "HIGH",
+ "CRITICAL"
+ ]
+ },
"time_stats": {
- "time_estimate": { "type": "integer" },
- "total_time_spent": { "type": "integer" },
- "human_time_estimate": { "type": ["string", "null"] },
- "human_total_time_spent": { "type": ["string", "null"] }
+ "time_estimate": {
+ "type": "integer"
+ },
+ "total_time_spent": {
+ "type": "integer"
+ },
+ "human_time_estimate": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "human_total_time_spent": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
},
"references": {
- "short": {"type": "string"},
- "relative": {"type": "string"},
- "full": {"type": "string"}
+ "short": {
+ "type": "string"
+ },
+ "relative": {
+ "type": "string"
+ },
+ "full": {
+ "type": "string"
+ }
}
},
"required": [
- "id", "iid", "project_id", "title", "description",
- "state", "created_at", "updated_at", "labels",
- "milestone", "assignees", "author", "user_notes_count",
- "upvotes", "downvotes", "due_date", "confidential",
+ "id",
+ "iid",
+ "project_id",
+ "title",
+ "description",
+ "state",
+ "created_at",
+ "updated_at",
+ "labels",
+ "milestone",
+ "assignees",
+ "author",
+ "user_notes_count",
+ "upvotes",
+ "downvotes",
+ "due_date",
+ "confidential",
"web_url"
]
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json
index c76806705e8..5ef97b020f9 100644
--- a/spec/fixtures/api/schemas/public_api/v4/issues.json
+++ b/spec/fixtures/api/schemas/public_api/v4/issues.json
@@ -1,9 +1,6 @@
{
"type": "array",
"items": {
- "type": "object",
- "properties" : {
- "$ref": "./issue.json"
- }
+ "$ref": "./issue.json"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/labels/label_with_counts.json b/spec/fixtures/api/schemas/public_api/v4/labels/label_with_counts.json
index 2331932e07d..4aa3199919b 100644
--- a/spec/fixtures/api/schemas/public_api/v4/labels/label_with_counts.json
+++ b/spec/fixtures/api/schemas/public_api/v4/labels/label_with_counts.json
@@ -1,16 +1,22 @@
{
"type": "object",
- "properties": {
- "allOf": [
- { "$ref": "label.json" },
- {
- "type": "object",
- "properties": {
- "open_issues_count": { "type": "integer" },
- "closed_issues_count": { "type": "integer" },
- "open_merge_requests_count": { "type": "integer" }
+ "allOf": [
+ {
+ "$ref": "label.json"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "open_issues_count": {
+ "type": "integer"
+ },
+ "closed_issues_count": {
+ "type": "integer"
+ },
+ "open_merge_requests_count": {
+ "type": "integer"
}
}
- ]
- }
-}
+ }
+ ]
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/labels/project_label.json b/spec/fixtures/api/schemas/public_api/v4/labels/project_label.json
index a9a065ee71f..ec4f64b6772 100644
--- a/spec/fixtures/api/schemas/public_api/v4/labels/project_label.json
+++ b/spec/fixtures/api/schemas/public_api/v4/labels/project_label.json
@@ -1,15 +1,22 @@
{
"type": "object",
- "properties": {
- "allOf": [
- { "$ref": "label.json" },
- {
- "type": "object",
- "properties": {
- "priority": { "type": ["integer", "null"] },
- "is_project_label": { "type": "boolean" }
+ "allOf": [
+ {
+ "$ref": "label.json"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "priority": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "is_project_label": {
+ "type": "boolean"
}
}
- ]
- }
-}
+ }
+ ]
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/labels/project_label_with_counts.json b/spec/fixtures/api/schemas/public_api/v4/labels/project_label_with_counts.json
index 87b90b2b3b5..cc0dc4024fe 100644
--- a/spec/fixtures/api/schemas/public_api/v4/labels/project_label_with_counts.json
+++ b/spec/fixtures/api/schemas/public_api/v4/labels/project_label_with_counts.json
@@ -1,9 +1,11 @@
{
"type": "object",
- "properties": {
- "allOf": [
- { "$ref": "project_label.json" },
- { "$ref": "label_with_counts.json" }
- ]
- }
-}
+ "allOf": [
+ {
+ "$ref": "project_label.json"
+ },
+ {
+ "$ref": "label_with_counts.json"
+ }
+ ]
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request.json b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
index 1ef2f9f9534..9a6c1757eea 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_request.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
@@ -1,25 +1,75 @@
{
"type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": "integer" },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "merged_by": { "$ref": "user/basic.json" },
- "merge_user": { "$ref": "user/basic.json" },
- "merged_at": { "type": ["string", "null"] },
- "closed_by": { "$ref": "user/basic.json" },
- "closed_at": { "type": ["string", "null"], "format": "date-time" },
- "created_at": { "type": "string", "format": "date-time" },
- "updated_at": { "type": "string", "format": "date-time" },
- "target_branch": { "type": "string" },
- "source_branch": { "type": "string" },
- "upvotes": { "type": "integer" },
- "downvotes": { "type": "integer" },
- "author": { "$ref": "user/basic.json" },
- "assignee": { "$ref": "user/basic.json" },
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "iid": {
+ "type": "integer"
+ },
+ "project_id": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "state": {
+ "type": "string"
+ },
+ "merged_by": {
+ "$ref": "user/basic.json"
+ },
+ "merge_user": {
+ "$ref": "user/basic.json"
+ },
+ "merged_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "closed_by": {
+ "$ref": "user/basic.json"
+ },
+ "closed_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "target_branch": {
+ "type": "string"
+ },
+ "source_branch": {
+ "type": "string"
+ },
+ "upvotes": {
+ "type": "integer"
+ },
+ "downvotes": {
+ "type": "integer"
+ },
+ "author": {
+ "$ref": "user/basic.json"
+ },
+ "assignee": {
+ "$ref": "user/basic.json"
+ },
"assignees": {
"type": "array",
"items": {
@@ -32,60 +82,159 @@
"$ref": "user/basic.json"
}
},
- "source_project_id": { "type": "integer" },
- "target_project_id": { "type": "integer" },
+ "source_project_id": {
+ "type": "integer"
+ },
+ "target_project_id": {
+ "type": "integer"
+ },
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
- "work_in_progress": { "type": "boolean" },
+ "work_in_progress": {
+ "type": "boolean"
+ },
"milestone": {
"oneOf": [
- { "type": "null" },
- { "$ref": "milestone.json" }
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "milestone.json"
+ }
]
},
- "merge_when_pipeline_succeeds": { "type": "boolean" },
- "merge_status": { "type": "string" },
- "sha": { "type": "string" },
- "merge_commit_sha": { "type": ["string", "null"] },
- "user_notes_count": { "type": "integer" },
- "changes_count": { "type": "string" },
- "should_remove_source_branch": { "type": ["boolean", "null"] },
- "force_remove_source_branch": { "type": ["boolean", "null"] },
- "discussion_locked": { "type": ["boolean", "null"] },
- "web_url": { "type": "uri" },
- "squash": { "type": "boolean" },
+ "merge_when_pipeline_succeeds": {
+ "type": "boolean"
+ },
+ "merge_status": {
+ "type": "string"
+ },
+ "sha": {
+ "type": "string"
+ },
+ "merge_commit_sha": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "user_notes_count": {
+ "type": "integer"
+ },
+ "changes_count": {
+ "type": "string"
+ },
+ "should_remove_source_branch": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "force_remove_source_branch": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "discussion_locked": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "squash": {
+ "type": "boolean"
+ },
"time_stats": {
- "time_estimate": { "type": "integer" },
- "total_time_spent": { "type": "integer" },
- "human_time_estimate": { "type": ["string", "null"] },
- "human_total_time_spent": { "type": ["string", "null"] }
+ "time_estimate": {
+ "type": "integer"
+ },
+ "total_time_spent": {
+ "type": "integer"
+ },
+ "human_time_estimate": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "human_total_time_spent": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "allow_collaboration": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "allow_maintainer_to_push": {
+ "type": [
+ "boolean",
+ "null"
+ ]
},
- "allow_collaboration": { "type": ["boolean", "null"] },
- "allow_maintainer_to_push": { "type": ["boolean", "null"] },
"references": {
- "short": {"type": "string"},
- "relative": {"type": "string"},
- "full": {"type": "string"}
+ "short": {
+ "type": "string"
+ },
+ "relative": {
+ "type": "string"
+ },
+ "full": {
+ "type": "string"
+ }
}
},
"required": [
- "id", "iid", "project_id", "title", "description",
- "state", "created_at", "updated_at", "target_branch",
- "source_branch", "upvotes", "downvotes", "author",
- "assignee", "source_project_id", "target_project_id",
- "labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds",
- "merge_status", "sha", "merge_commit_sha", "user_notes_count",
- "should_remove_source_branch", "force_remove_source_branch",
- "web_url", "squash"
+ "id",
+ "iid",
+ "project_id",
+ "title",
+ "description",
+ "state",
+ "created_at",
+ "updated_at",
+ "target_branch",
+ "source_branch",
+ "upvotes",
+ "downvotes",
+ "author",
+ "assignee",
+ "source_project_id",
+ "target_project_id",
+ "labels",
+ "work_in_progress",
+ "milestone",
+ "merge_when_pipeline_succeeds",
+ "merge_status",
+ "sha",
+ "merge_commit_sha",
+ "user_notes_count",
+ "should_remove_source_branch",
+ "force_remove_source_branch",
+ "web_url",
+ "squash"
],
"head_pipeline": {
"oneOf": [
- { "type": "null" },
- { "$ref": "pipeline/detail.json" }
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "pipeline/detail.json"
+ }
]
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request_simple.json b/spec/fixtures/api/schemas/public_api/v4/merge_request_simple.json
index f176e5ee261..48de8f13547 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_request_simple.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_request_simple.json
@@ -1,26 +1,59 @@
{
"type": "object",
- "properties" : {
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": "integer" },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "created_at": { "type": "string", "format": "date-time" },
- "updated_at": { "type": "string", "format": "date-time" },
- "web_url": { "type": "uri" }
- },
- "required": [
- "id", "iid", "project_id", "title", "description",
- "state", "created_at", "updated_at", "web_url"
- ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "iid": {
+ "type": "integer"
+ },
+ "project_id": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "state": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ },
"head_pipeline": {
"oneOf": [
- { "type": "null" },
- { "$ref": "pipeline/detail.json" }
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "pipeline/detail.json"
+ }
]
}
- }
-}
+ },
+ "required": [
+ "id",
+ "iid",
+ "project_id",
+ "title",
+ "description",
+ "state",
+ "created_at",
+ "updated_at",
+ "web_url"
+ ]
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/milestone.json b/spec/fixtures/api/schemas/public_api/v4/milestone.json
index e7e0e57f02f..c33c4044a62 100644
--- a/spec/fixtures/api/schemas/public_api/v4/milestone.json
+++ b/spec/fixtures/api/schemas/public_api/v4/milestone.json
@@ -1,24 +1,77 @@
{
"type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": ["integer", "null"] },
- "group_id": { "type": ["integer", "null"] },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "created_at": { "type": "string" },
- "updated_at": { "type": "string" },
- "start_date": { "type": ["string", "null"], "format": "date-time" },
- "due_date": { "type": ["string", "null"], "format": "date-time" },
- "expired": { "type": ["boolean", "null"] },
- "web_url": { "type": "string" }
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "iid": {
+ "type": "integer"
+ },
+ "project_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "group_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "state": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "updated_at": {
+ "type": "string"
+ },
+ "start_date": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "due_date": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "expired": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "web_url": {
+ "type": "string"
+ }
},
"required": [
- "id", "iid", "title", "description", "state",
- "state", "created_at", "updated_at", "start_date",
- "due_date", "expired"
+ "id",
+ "iid",
+ "title",
+ "description",
+ "state",
+ "created_at",
+ "updated_at",
+ "start_date",
+ "due_date",
+ "expired"
],
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/milestone_with_stats.json b/spec/fixtures/api/schemas/public_api/v4/milestone_with_stats.json
index d09d1634eb9..b5fa161ec2a 100644
--- a/spec/fixtures/api/schemas/public_api/v4/milestone_with_stats.json
+++ b/spec/fixtures/api/schemas/public_api/v4/milestone_with_stats.json
@@ -1,32 +1,95 @@
{
"type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": ["integer", "null"] },
- "group_id": { "type": ["integer", "null"] },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "created_at": { "type": "string", "format": "date-time" },
- "updated_at": { "type": "string", "format": "date-time" },
- "start_date": { "type": ["string", "null"], "format": "date-time" },
- "due_date": { "type": ["string", "null"], "format": "date-time" },
- "expired": { "type": ["boolean", "null"] },
- "web_url": { "type": "string" },
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "iid": {
+ "type": "integer"
+ },
+ "project_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "group_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "state": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "start_date": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "due_date": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "expired": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "web_url": {
+ "type": "string"
+ },
"issue_stats": {
- "required": ["total", "closed"],
+ "required": [
+ "total",
+ "closed"
+ ],
"properties": {
- "total": { "type": "integer" },
- "closed": { "type": "integer" }
+ "total": {
+ "type": "integer"
+ },
+ "closed": {
+ "type": "integer"
+ }
},
"additionalProperties": false
}
},
"required": [
- "id", "iid", "title", "description", "state",
- "state", "created_at", "updated_at", "start_date",
- "due_date", "expired", "issue_stats"
+ "id",
+ "iid",
+ "title",
+ "description",
+ "state",
+ "created_at",
+ "updated_at",
+ "start_date",
+ "due_date",
+ "expired",
+ "issue_stats"
],
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/notes.json b/spec/fixtures/api/schemas/public_api/v4/notes.json
index d6d0300a64f..1987a0f2f71 100644
--- a/spec/fixtures/api/schemas/public_api/v4/notes.json
+++ b/spec/fixtures/api/schemas/public_api/v4/notes.json
@@ -2,43 +2,124 @@
"type": "array",
"items": {
"type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "type": { "type": ["string", "null"] },
- "body": { "type": "string" },
- "attachment": { "type": ["string", "null"] },
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "type": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "body": {
+ "type": "string"
+ },
+ "attachment": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"author": {
"type": "object",
"properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
+ "name": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "state": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ }
},
- "required" : [
- "id", "name", "username", "state", "avatar_url", "web_url"
+ "required": [
+ "id",
+ "name",
+ "username",
+ "state",
+ "avatar_url",
+ "web_url"
]
},
- "commands_changes": { "type": "object", "additionalProperties": true },
- "created_at": { "type": "string", "format": "date-time" },
- "updated_at": { "type": "string", "format": "date-time" },
- "system": { "type": "boolean" },
- "noteable_id": { "type": "integer" },
- "noteable_iid": { "type": "integer" },
- "noteable_type": { "type": "string" },
- "resolved": { "type": "boolean" },
- "resolvable": { "type": "boolean" },
- "resolved_by": { "type": ["string", "null"] },
- "resolved_at": { "type": ["string", "null"] },
- "confidential": { "type": ["boolean", "null"] },
- "internal": { "type": ["boolean", "null"] }
+ "commands_changes": {
+ "type": "object",
+ "additionalProperties": true
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "system": {
+ "type": "boolean"
+ },
+ "noteable_id": {
+ "type": "integer"
+ },
+ "noteable_iid": {
+ "type": "integer"
+ },
+ "noteable_type": {
+ "type": "string"
+ },
+ "resolved": {
+ "type": "boolean"
+ },
+ "resolvable": {
+ "type": "boolean"
+ },
+ "resolved_by": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "resolved_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "confidential": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "internal": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ }
},
"required": [
- "id", "body", "attachment", "author", "created_at", "updated_at",
- "system", "noteable_id", "noteable_type"
+ "id",
+ "body",
+ "attachment",
+ "author",
+ "created_at",
+ "updated_at",
+ "system",
+ "noteable_id",
+ "noteable_type"
],
"additionalProperties": false
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/package.json b/spec/fixtures/api/schemas/public_api/v4/packages/package.json
index 5d0d5f63aa9..c1b4fc09079 100644
--- a/spec/fixtures/api/schemas/public_api/v4/packages/package.json
+++ b/spec/fixtures/api/schemas/public_api/v4/packages/package.json
@@ -28,12 +28,9 @@
},
"_links": {
"type": "object",
- "required": [
- "web_path"
- ],
"properties": {
- "details": {
- "type": "string"
+ "web_path": {
+ "type": ["string", "null"]
}
}
},
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
index 66d4be529b1..9bcd191741f 100644
--- a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
@@ -1,23 +1,60 @@
{
"type": "object",
"properties": {
- "domain": { "type": "string" },
- "url": { "type": "uri" },
- "project_id": { "type": "integer" },
- "verified": { "type": "boolean" },
- "verification_code": { "type": ["string", "null"] },
- "enabled_until": { "type": ["string", "null"], "format": "date-time" },
- "auto_ssl_enabled": { "type": "boolean" },
+ "domain": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "project_id": {
+ "type": "integer"
+ },
+ "verified": {
+ "type": "boolean"
+ },
+ "verification_code": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "enabled_until": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "auto_ssl_enabled": {
+ "type": "boolean"
+ },
"certificate_expiration": {
"type": "object",
"properties": {
- "expired": { "type": "boolean" },
- "expiration": { "type": "string" }
+ "expired": {
+ "type": "boolean"
+ },
+ "expiration": {
+ "type": "string"
+ }
},
- "required": ["expired", "expiration"],
+ "required": [
+ "expired",
+ "expiration"
+ ],
"additionalProperties": false
}
},
- "required": ["domain", "url", "project_id", "verified", "verification_code", "enabled_until", "auto_ssl_enabled"],
+ "required": [
+ "domain",
+ "url",
+ "project_id",
+ "verified",
+ "verification_code",
+ "enabled_until",
+ "auto_ssl_enabled"
+ ],
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json
index bbbc610eb27..6ce431a274a 100644
--- a/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json
@@ -1,24 +1,61 @@
{
"type": "object",
"properties": {
- "domain": { "type": "string" },
- "url": { "type": "uri" },
- "verified": { "type": "boolean" },
- "verification_code": { "type": ["string", "null"] },
- "enabled_until": { "type": ["string", "null"] },
- "auto_ssl_enabled": { "type": "boolean" },
+ "domain": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "verified": {
+ "type": "boolean"
+ },
+ "verification_code": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "enabled_until": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "auto_ssl_enabled": {
+ "type": "boolean"
+ },
"certificate": {
"type": "object",
"properties": {
- "subject": { "type": "string" },
- "expired": { "type": "boolean" },
- "certificate": { "type": "string" },
- "certificate_text": { "type": "string" }
+ "subject": {
+ "type": "string"
+ },
+ "expired": {
+ "type": "boolean"
+ },
+ "certificate": {
+ "type": "string"
+ },
+ "certificate_text": {
+ "type": "string"
+ }
},
- "required": ["subject", "expired"],
+ "required": [
+ "subject",
+ "expired"
+ ],
"additionalProperties": false
}
},
- "required": ["domain", "url", "verified", "verification_code", "enabled_until", "auto_ssl_enabled"],
+ "required": [
+ "domain",
+ "url",
+ "verified",
+ "verification_code",
+ "enabled_until",
+ "auto_ssl_enabled"
+ ],
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/project_hooks.json b/spec/fixtures/api/schemas/public_api/v4/project_hooks.json
index 8c542ebe3ad..8557e5cae40 100644
--- a/spec/fixtures/api/schemas/public_api/v4/project_hooks.json
+++ b/spec/fixtures/api/schemas/public_api/v4/project_hooks.json
@@ -1,10 +1,6 @@
{
"type": "array",
"items": {
- "type": "object",
- "properties" : {
- "$ref": "./project_hook.json"
- }
+ "$ref": "./project_hook.json"
}
-}
-
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/snippets.json b/spec/fixtures/api/schemas/public_api/v4/snippets.json
index 65299901128..dab6b69f1c1 100644
--- a/spec/fixtures/api/schemas/public_api/v4/snippets.json
+++ b/spec/fixtures/api/schemas/public_api/v4/snippets.json
@@ -2,46 +2,107 @@
"type": "array",
"items": {
"type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "project_id": { "type": ["integer", "null"] },
- "title": { "type": "string" },
- "file_name": { "type": ["string", "null"] },
- "files" : {
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "project_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "title": {
+ "type": "string"
+ },
+ "file_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "files": {
"type": "array",
"items": {
"type": "object",
"properties": {
- "path": { "type": "string" },
- "raw_url": { "type": "string" }
+ "path": {
+ "type": "string"
+ },
+ "raw_url": {
+ "type": "string"
+ }
}
}
},
- "description": { "type": ["string", "null"] },
- "visibility": { "type": "string" },
- "web_url": { "type": "string" },
- "raw_url": { "type": "string" },
- "created_at": { "type": "string", "format": "date-time" },
- "updated_at": { "type": "string", "format": "date-time" },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "visibility": {
+ "type": "string"
+ },
+ "web_url": {
+ "type": "string"
+ },
+ "raw_url": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
"author": {
"type": "object",
"properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
+ "name": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "state": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ }
},
- "required" : [
- "id", "name", "username", "state", "avatar_url", "web_url"
+ "required": [
+ "id",
+ "name",
+ "username",
+ "state",
+ "avatar_url",
+ "web_url"
]
}
},
"required": [
- "id", "title", "file_name", "description", "web_url",
- "created_at", "updated_at", "author", "raw_url"
+ "id",
+ "title",
+ "file_name",
+ "description",
+ "web_url",
+ "created_at",
+ "updated_at",
+ "author",
+ "raw_url"
],
"additionalProperties": false
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/system_hooks.json b/spec/fixtures/api/schemas/public_api/v4/system_hooks.json
index a56542a8b99..a0800e4bc97 100644
--- a/spec/fixtures/api/schemas/public_api/v4/system_hooks.json
+++ b/spec/fixtures/api/schemas/public_api/v4/system_hooks.json
@@ -1,9 +1,6 @@
{
"type": "array",
"items": {
- "type": "object",
- "properties" : {
- "$ref": "./system_hook.json"
- }
+ "$ref": "./system_hook.json"
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/admin.json b/spec/fixtures/api/schemas/public_api/v4/user/admin.json
index f0d3cf3ba0e..7b46cbf5a36 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/admin.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/admin.json
@@ -32,6 +32,148 @@
"namespace_id"
],
"properties": {
- "$ref": "full.json"
+ "id": {
+ "type": "integer"
+ },
+ "username": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string",
+ "pattern": "^[^@]+@[^@]+$"
+ },
+ "commit_email": {
+ "type": "string",
+ "pattern": "^[^@]+@[^@]+$"
+ },
+ "name": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string",
+ "enum": [
+ "active",
+ "blocked"
+ ]
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "uri"
+ },
+ "web_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "is_admin": {
+ "type": "boolean"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "bio": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "location": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "pronouns": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "skype": {
+ "type": "string"
+ },
+ "linkedin": {
+ "type": "string"
+ },
+ "twitter": {
+ "type": "string"
+ },
+ "website_url": {
+ "type": "string"
+ },
+ "organization": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "last_sign_in_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "confirmed_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "color_scheme_id": {
+ "type": "integer"
+ },
+ "projects_limit": {
+ "type": "integer"
+ },
+ "current_sign_in_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "identities": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "provider": {
+ "type": "string",
+ "enum": [
+ "github",
+ "bitbucket",
+ "google_oauth2",
+ "twitter"
+ ]
+ },
+ "extern_uid": {
+ "type": [
+ "number",
+ "string"
+ ]
+ }
+ }
+ }
+ },
+ "can_create_group": {
+ "type": "boolean"
+ },
+ "can_create_project": {
+ "type": "boolean"
+ },
+ "two_factor_enabled": {
+ "type": "boolean"
+ },
+ "external": {
+ "type": "boolean"
+ },
+ "namespace_id": {
+ "type": "integer"
+ }
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json
deleted file mode 100644
index aa066883c47..00000000000
--- a/spec/fixtures/api/schemas/public_api/v4/user/login.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "type": "object",
- "required": [
- "id",
- "username",
- "email",
- "name",
- "state",
- "avatar_url",
- "web_url",
- "created_at",
- "is_admin",
- "bio",
- "location",
- "skype",
- "linkedin",
- "twitter",
- "website_url",
- "organization",
- "last_sign_in_at",
- "confirmed_at",
- "theme_id",
- "color_scheme_id",
- "projects_limit",
- "current_sign_in_at",
- "identities",
- "can_create_group",
- "can_create_project",
- "two_factor_enabled",
- "external"
- ],
- "properties": {
- "$ref": "full.json"
- }
-}
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/public.json b/spec/fixtures/api/schemas/public_api/v4/user/public.json
index c4549e3ef63..53d67e041a1 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/public.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/public.json
@@ -28,32 +28,95 @@
"external"
],
"properties": {
- "id": { "type": "integer" },
- "username": { "type": "string" },
+ "id": {
+ "type": "integer"
+ },
+ "username": {
+ "type": "string"
+ },
"email": {
"type": "string",
"pattern": "^[^@]+@[^@]+$"
},
- "name": { "type": "string" },
+ "name": {
+ "type": "string"
+ },
"state": {
"type": "string",
- "enum": ["active", "blocked"]
- },
- "avatar_url": { "type": [ "string", "null" ] },
- "web_url": { "type": "string" },
- "created_at": { "type": "string", "format": "date-time" },
- "bio": { "type": ["string", "null"] },
- "location": { "type": ["string", "null"] },
- "skype": { "type": "string" },
- "linkedin": { "type": "string" },
- "twitter": { "type": "string "},
- "website_url": { "type": "string" },
- "organization": { "type": ["string", "null"] },
- "last_sign_in_at": { "type": ["string", "null"], "format": "date-time" },
- "confirmed_at": { "type": ["string", "null"] },
- "color_scheme_id": { "type": "integer" },
- "projects_limit": { "type": "integer" },
- "current_sign_in_at": { "type": ["string", "null"], "format": "date-time" },
+ "enum": [
+ "active",
+ "blocked"
+ ]
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "web_url": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "bio": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "location": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "skype": {
+ "type": "string"
+ },
+ "linkedin": {
+ "type": "string"
+ },
+ "twitter": {
+ "type": "string"
+ },
+ "website_url": {
+ "type": "string"
+ },
+ "organization": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "last_sign_in_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "confirmed_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "color_scheme_id": {
+ "type": "integer"
+ },
+ "projects_limit": {
+ "type": "integer"
+ },
+ "current_sign_in_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
"identities": {
"type": "array",
"items": {
@@ -61,16 +124,35 @@
"properties": {
"provider": {
"type": "string",
- "enum": ["github", "bitbucket", "google_oauth2"]
+ "enum": [
+ "github",
+ "bitbucket",
+ "google_oauth2"
+ ]
},
- "extern_uid": { "type": ["number", "string"] }
+ "extern_uid": {
+ "type": [
+ "number",
+ "string"
+ ]
+ }
}
}
},
- "can_create_group": { "type": "boolean" },
- "can_create_project": { "type": "boolean" },
- "two_factor_enabled": { "type": "boolean" },
- "external": { "type": "boolean" },
- "commit_email": { "type": "string" }
+ "can_create_group": {
+ "type": "boolean"
+ },
+ "can_create_project": {
+ "type": "boolean"
+ },
+ "two_factor_enabled": {
+ "type": "boolean"
+ },
+ "external": {
+ "type": "boolean"
+ },
+ "commit_email": {
+ "type": "string"
+ }
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/registry/repository.json b/spec/fixtures/api/schemas/registry/repository.json
index 18d2c68ac2f..078fba7a34a 100644
--- a/spec/fixtures/api/schemas/registry/repository.json
+++ b/spec/fixtures/api/schemas/registry/repository.json
@@ -1,6 +1,12 @@
{
"type": "object",
- "required": ["id", "name", "path", "location", "created_at"],
+ "required": [
+ "id",
+ "name",
+ "path",
+ "location",
+ "created_at"
+ ],
"properties": {
"id": {
"type": "integer"
@@ -18,10 +24,15 @@
"type": "string"
},
"created_at": {
- "type": "date-time"
+ "type": "string",
+ "format": "date-time"
},
"cleanup_policy_started_at": {
- "type": "date-time"
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
},
"tags_path": {
"type": "string"
@@ -31,14 +42,24 @@
},
"status": {
"oneOf": [
- { "type": "null" },
- { "type": "string", "enum": ["delete_scheduled", "delete_failed"] }
+ {
+ "type": "null"
+ },
+ {
+ "type": "string",
+ "enum": [
+ "delete_scheduled",
+ "delete_failed"
+ ]
+ }
]
},
- "tags": { "$ref": "tags.json" },
+ "tags": {
+ "$ref": "tags.json"
+ },
"tags_count": {
"type": "integer"
}
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/release.json b/spec/fixtures/api/schemas/release.json
index fe4f8cd2157..11f238ffc92 100644
--- a/spec/fixtures/api/schemas/release.json
+++ b/spec/fixtures/api/schemas/release.json
@@ -1,22 +1,56 @@
{
"type": "object",
- "required": ["tag_name", "description"],
+ "required": [
+ "tag_name",
+ "description"
+ ],
"properties": {
- "name": { "type": "string" },
- "tag_name": { "type": "string" },
- "ref": { "type": "string "},
- "description": { "type": "string" },
- "description_html": { "type": "string" },
- "created_at": { "type": "string", "format": "date-time" },
+ "name": {
+ "type": "string"
+ },
+ "tag_name": {
+ "type": "string"
+ },
+ "ref": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "description_html": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
"commit": {
- "oneOf": [{ "type": "null" }, { "$ref": "public_api/v4/commit/basic.json" }]
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "public_api/v4/commit/basic.json"
+ }
+ ]
},
"author": {
- "oneOf": [{ "type": "null" }, { "$ref": "public_api/v4/user/basic.json" }]
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "public_api/v4/user/basic.json"
+ }
+ ]
},
"assets": {
- "count": { "type": "integer" },
- "links": { "$ref": "release/links.json" },
+ "count": {
+ "type": "integer"
+ },
+ "links": {
+ "$ref": "release/links.json"
+ },
"sources": {
"type": "array",
"items": {
@@ -27,4 +61,4 @@
}
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/variable.json b/spec/fixtures/api/schemas/variable.json
index c49d7d8c5ea..440e812d95b 100644
--- a/spec/fixtures/api/schemas/variable.json
+++ b/spec/fixtures/api/schemas/variable.json
@@ -8,13 +8,31 @@
"protected"
],
"properties": {
- "id": { "type": "integer" },
- "key": { "type": "string" },
- "value": { "type": "string" },
- "masked": { "type": "boolean" },
- "protected": { "type": "boolean" },
- "variable_type": { "type": "string" },
- "environment_scope": { "type": "string", "optional": true }
+ "id": {
+ "type": "integer"
+ },
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ },
+ "masked": {
+ "type": "boolean"
+ },
+ "protected": {
+ "type": "boolean"
+ },
+ "raw": {
+ "type": "boolean"
+ },
+ "variable_type": {
+ "type": "string"
+ },
+ "environment_scope": {
+ "type": "string",
+ "optional": true
+ }
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/ce_sample_schema.json b/spec/fixtures/ce_sample_schema.json
index e69de29bb2d..9e26dfeeb6e 100644
--- a/spec/fixtures/ce_sample_schema.json
+++ b/spec/fixtures/ce_sample_schema.json
@@ -0,0 +1 @@
+{} \ No newline at end of file
diff --git a/spec/fixtures/clusters/chain_certificates.pem b/spec/fixtures/clusters/chain_certificates.pem
index 63f0b812b66..98ee90046f3 100644
--- a/spec/fixtures/clusters/chain_certificates.pem
+++ b/spec/fixtures/clusters/chain_certificates.pem
@@ -1,85 +1,63 @@
-----BEGIN CERTIFICATE-----
-MIIGYjCCBUqgAwIBAgIQATfkha/xTr3pbLHVWlPq4jANBgkqhkiG9w0BAQsFADBY
-MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEuMCwGA1UE
-AxMlR2xvYmFsU2lnbiBBdGxhcyBSMyBEViBUTFMgQ0EgMjAyMiBRMzAeFw0yMjA3
-MjIxOTQyMTFaFw0yMzA4MjMxOTQyMTBaMBsxGTAXBgNVBAMMEGFib3V0LmdpdGxh
-Yi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFFFQs8EITaWo5
-0U18/mPTDLencU/7siJT/4P8oeDkemyx98wzK6vuNj/2JEZ3v1psKun5n8Pb/fHa
-somKd/4icHgC4rnxrO6zayfb+cKzVghQe12Nj75lx6RtppqTgAmSOa3Tai5niICT
-I8s3d2wsHtfEgqAavcD0/zdPIk25Ji7yfquldSthnlhQqI4Pm3OxTiyFj/V5ZhFl
-IWZLvQaENjBSDVZQDcaPdWwodfXNA8fJmqk7cTLQ9P9NgjWvva7acl+Yd6hOFzV0
-EllBl/WF1KB+YzGuHI0CQHT7sv3GW1lXeE2EqrWoSdLTOSAqm6y02DyE79d1xvG6
-XXfX5ILlAgMBAAGjggNjMIIDXzAbBgNVHREEFDASghBhYm91dC5naXRsYWIuY29t
-MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw
-HQYDVR0OBBYEFHK7MnjGDptQWjfmJ2fr3IrjxEopMFcGA1UdIARQME4wCAYGZ4EM
-AQIBMEIGCisGAQQBoDIKAQMwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly93d3cuZ2xv
-YmFsc2lnbi5jb20vcmVwb3NpdG9yeS8wDAYDVR0TAQH/BAIwADCBngYIKwYBBQUH
-AQEEgZEwgY4wQAYIKwYBBQUHMAGGNGh0dHA6Ly9vY3NwLmdsb2JhbHNpZ24uY29t
-L2NhL2dzYXRsYXNyM2R2dGxzY2EyMDIycTMwSgYIKwYBBQUHMAKGPmh0dHA6Ly9z
-ZWN1cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0L2dzYXRsYXNyM2R2dGxzY2EyMDIy
-cTMuY3J0MB8GA1UdIwQYMBaAFPqROWOa+60QJOW+tbnaq9nERmmrMEgGA1UdHwRB
-MD8wPaA7oDmGN2h0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20vY2EvZ3NhdGxhc3Iz
-ZHZ0bHNjYTIwMjJxMy5jcmwwggF9BgorBgEEAdZ5AgQCBIIBbQSCAWkBZwB1AG9T
-dqwx8DEZ2JkApFEV/3cVHBHZAsEAKQaNsgiaN9kTAAABgiduiHEAAAQDAEYwRAIg
-SYQrru/KAKfe+hUqpJmk7Fc8drkgtY3IcAurTOwbM68CIBYO9sbDspd5p7v17RQi
-QQkjdRwSjHiIgvlX0Y1JqmXjAHYArfe++nz/EMiLnT2cHj4YarRnKV3PsQwkyoWG
-NOvcgooAAAGCJ26IcQAABAMARzBFAiBc5a10annqMEH69bdEFy/Vo1gb3S3GQ993
-BCRV7ZXG4gIhAMqnsoKkU6ITwRXwE9KGjHnijJ8QrBrnK0i+JFaGe1ffAHYAs3N3
-B+GEUPhjhtYFqdwRCUp5LbFnDAuH3PADDnk2pZoAAAGCJ26I6QAABAMARzBFAiAf
-lW8Agd0DB68YA8XAbnlq7QNHw3uRMzNdS8gtRUe75gIhANTe+mt2p1ryW83P31OW
-jH3cEGJxdUNT/oDM3Fzesx94MA0GCSqGSIb3DQEBCwUAA4IBAQAfivKEmjqqOFFh
-VsX2XYkoDtreghpqMwHMCLwNk852Alr/Seyv9Ilng8cunU4NmhvEtsYVXkfE4XvB
-0QIVxkg1w7A+p7ejMjh6doLJ0aWNWIVW/DwOeP0qstF9lqvLdLDABoVn0BtYCDTH
-gjG80e2xpvPiKHGvBL+hlOIJwUuIAT3jN23sS1GoiYQGKsz0lovB09/6MGG0Qj8C
-3i9a59T9XBpwSKdpKd4u/CB6koBXD3atbBNBACuAMcFckTEtmkCFtSpqBuocJGKf
-LB4MFVaEwrd7Lc1ACC1et5FDtEI4I3/CerkRZTV+mRz5n6tB91AK3dRvjElfhiuh
-XXYRULvB
+MIIDgDCCAmigAwIBAgIJALI40QyGMz+AMA0GCSqGSIb3DQEBCwUAMHExCzAJBgNV
+BAYTAlVTMRYwFAYDVQQIDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQHDApDYWxpZm9y
+bmlhMRYwFAYDVQQKDA1HaXRMYWIgKFRlc3QpMR0wGwYDVQQDDBRUZXN0IEludGVy
+bWVkaWF0ZSBDQTAgFw0yMjExMjEwMDAzMTZaGA8zMDIyMDMyNDAwMDMxNlowazEL
+MAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNh
+bGlmb3JuaWExFjAUBgNVBAoMDUdpdExhYiAoVGVzdCkxFzAVBgNVBAMMDlRlc3Qg
+TGVhZiBDZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxPHRlMks
+Auba0UxFoL248jfCQNWpEbRoImj/B9W5Zm2sdmJpaD01NVfR+O7eovbWozN6J+gs
+MKS5SUVDIu0E5iVBsEsQlDAF+ckPo3kvoWDAF2cLgNjpRApIiS9YCy9dZLxUl34x
+F4nXy19+53mouw/tX7kPLwnHxTVvBEpDfIttOCTlXnLu62v2LRP5lt5vFXE0B/00
+bD6t+lVl7eamvjDRAz2HoFMY+m1Popts7j4Pj3rWVWfo1RRu88NvjQLnqccFQFnd
+tDsv3YqtiWDdCbcnkIKcgZ3ckpDhTKwRjnKmub4un/OpEgn/WVR1iLCOCDS1RX1u
+j5HKhyiEzRJt1QIDAQABox8wHTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t
+MA0GCSqGSIb3DQEBCwUAA4IBAQA31V60ppRL+/YD1qlGu2uDXKn2VaK41LQxoz7R
+LCQp6qf50XhQFMBu2Mdf5cAbMdgIcAE6BQ7r7kaxO4dSXn9J3cy5Qb4msRvGy63P
+rm0G71TmAK3ihYLP19eT5g9oOHarRc3gXVtUxCAd/XVrxp1GW2vxKTGo46rKKVeU
+myU3Yn9hlu6VszvyaseaDo4Tvd6+vfdpGRo6lcJIdjcQ3Ie2bH6kF1RWXT2sgBW/
+XsDrF7O15DUE9na/7ak8tCOnxFaag7f7SLEHC+NSiMkrRry7Yc3WEFMeJTzHCXGn
+5Uwl4FKujmFkTwOPOeKyWIkDJg+PQVXnVrTvZlx+6k+Kpv2j
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
-MIIEjzCCA3egAwIBAgIQfCoMIT/GVVNFyR8ZH7hO+jANBgkqhkiG9w0BAQsFADBM
-MSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xv
-YmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0yMjA0MjAxMjAwMDBaFw0y
-NTA0MjAwMDAwMDBaMFgxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWdu
-IG52LXNhMS4wLAYDVQQDEyVHbG9iYWxTaWduIEF0bGFzIFIzIERWIFRMUyBDQSAy
-MDIyIFEzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuKh6ZjxOZpzO
-N6VUNU02x5nTqCc28i/G1Rg+6QndBdbXLDQyfAhjSdEQN+V4XRFizm37Lz83lNuP
-ezDpXizZVT+y27mgtWA3i6QGMjVQpAmvCkX/qB+bZY7dSuBAoeNjN1iQ3XU7/A4c
-gkCYvXCxwUgUFDwES2nd1JwBpukh44IK/uSqvzSgjMvJeW4+XGpSnsTtK8Vp/lA8
-k521/y0oqGwGbJ3Fr7JZ+1l3DXR6iISk1B3UuiAGzLUeSE50IRWGdcDMWtEFz1cW
-ehMX7MJKrtUecqoiWoycgjLEEOZCbiGGaHyAIzA1072wXgopK/AUsRg32Vklw+c4
-2enULTY1ZQIDAQABo4IBXzCCAVswDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQG
-CCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW
-BBT6kTljmvutECTlvrW52qvZxEZpqzAfBgNVHSMEGDAWgBSP8Et/qC5FJK5NUPpj
-move4t0bvDB7BggrBgEFBQcBAQRvMG0wLgYIKwYBBQUHMAGGImh0dHA6Ly9vY3Nw
-Mi5nbG9iYWxzaWduLmNvbS9yb290cjMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9zZWN1
-cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0L3Jvb3QtcjMuY3J0MDYGA1UdHwQvMC0w
-K6ApoCeGJWh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20vcm9vdC1yMy5jcmwwIQYD
-VR0gBBowGDAIBgZngQwBAgEwDAYKKwYBBAGgMgoBAzANBgkqhkiG9w0BAQsFAAOC
-AQEAFDMseeU/gsZwP9pZOKe7onasYRgFaFfZDfuKRrzxqOgMcAIdxi+X7TY+nlKG
-L1xi2NVHQ5pz0Sslh59EtBTrJrwhR3QgvZ+kv7OAHU01fc25tdpV8pBQyLIXTg60
-YYgpX0RdA39XkYHQ6zCu1SrsgiDOTtKwi5UCYXPYaTT0rWMOXOQgH6l97Y7lHAS7
-Ip/HqSLKmT0Cp2foBi36BGu7SdJsmVdjbC3CYXjhILH79r/hgjk5PHvvfRqVSrJy
-2lWQru3d4nCQfBrutTJaXc/W+kXyngEMMS+JhP4xYA/97qZbhNXHGOak+UAwKRge
-/vxBtbkpBXWLYhpbIi6/5FlssA==
+MIIDgjCCAmqgAwIBAgIJAMTV0ieHng2/MA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
+BAYTAlVTMRYwFAYDVQQIDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQHDApDYWxpZm9y
+bmlhMRYwFAYDVQQKDA1HaXRMYWIgKFRlc3QpMRUwEwYDVQQDDAxUZXN0IFJvb3Qg
+Q0EwIBcNMjIxMTIwMjMzNzQyWhgPMzAyMjAzMjMyMzM3NDJaMHExCzAJBgNVBAYT
+AlVTMRYwFAYDVQQIDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQHDApDYWxpZm9ybmlh
+MRYwFAYDVQQKDA1HaXRMYWIgKFRlc3QpMR0wGwYDVQQDDBRUZXN0IEludGVybWVk
+aWF0ZSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJfazJfCLjY9
+wGUfI5MxWtyX6H/DAAiYNSfRw28M4ZBAwJcgClvwEHeRFGVOw1TwDyopMV3JIDtk
++87AGvF1Jyu+5k+XkJtOmb6HtotjelUJS1gCgzbBoqlfHTfFCYNRD/rWV1zBFkq5
+wTMeRQvy7Q5MocBzr2VHwFvBr1YkT5P8nSsQ2iFA3r7Lsv5csgj1D7yFuI6LQcWN
+3IpCORuU7144ZNuK3ChhcmrV9S2770qAgqdmSUpIYQO4xCOFJB3Zs7kHJkKXn1cz
+jDXFsbQ//ouKG8zBDzeCawNsHMVei2oXBySZYSVXaHI1fRB9MYIAdheCn5QQSyqG
+zN7mP03iQq8CAwEAAaMjMCEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AYYwDQYJKoZIhvcNAQELBQADggEBAJaXhgFrG4ijA/TinY2ApARY5lsk0ol8DKq7
+4hVDDudRo1ET5gW3VhbYIkN9r+hZeRnzf3WghC75si7WROpv2DeNKKv2FzFg4OqF
+nw154EVvEWJ8tyCTZ/8tqIBErEJUPfzuO8bZjVpN+2eSVDbo3UBLVFbsE37is7li
+eqjic+kmS6onvlG7xN9xCAwo7zGsyfceTLC9n3D8kfJd1YYV8I/l0jU1Gxf6S7g4
+2DnqNaOGTBwdioIAhaUB4amXw4NZ/NZhG3bVsNyWt99cvtLeMFTSl9lRRMT3xxCI
++TPaT30R9qRuI1Sd0IxdybSx5QG6AuZAe/5v+2+C4+8u6onpuXE=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
-MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G
-A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp
-Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4
-MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG
-A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
-hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8
-RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT
-gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm
-KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd
-QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ
-XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw
-DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o
-LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU
-RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp
-jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK
-6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX
-mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs
-Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
-WD9f
+MIIDejCCAmKgAwIBAgIJAMMTWTnLVjZwMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
+BAYTAlVTMRYwFAYDVQQIDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQHDApDYWxpZm9y
+bmlhMRYwFAYDVQQKDA1HaXRMYWIgKFRlc3QpMRUwEwYDVQQDDAxUZXN0IFJvb3Qg
+Q0EwIBcNMjIxMTIwMjMyMzA4WhgPMzAyMjAzMjMyMzIzMDhaMGkxCzAJBgNVBAYT
+AlVTMRYwFAYDVQQIDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQHDApDYWxpZm9ybmlh
+MRYwFAYDVQQKDA1HaXRMYWIgKFRlc3QpMRUwEwYDVQQDDAxUZXN0IFJvb3QgQ0Ew
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFe5BkdGSwAL1w7kEMgMVu
+quanndVPI7Mz5YthrmJP1Qgv5TY7440PrKKbtSqx4aS2KdNKq9th7sHZ6Qe6NKbq
+G6/HZPCVO+X7ORI8yGbWD1iYBLCXmMOkXY5UsDcN5z/oKRXzHfLD0uYSQ5Fp0D9J
+O9Y0I2iz1ywsGm784xt0n7Yzz3HgjvYwU2VdLJPSzwSBPphj64E2cfM+5rGoaJd7
+Er3FodqR0YegrsMmggpK/QPIylt+TCkTgBL/+paM4KynUYusB+LzqXm0u1mYuZmt
++9Y4sKVZiNKlQFfg9XfZRpldn8pnFBMnLaUU757+mPJOuy5mLVlwAntw9qgi6UfX
+AgMBAAGjIzAhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqG
+SIb3DQEBCwUAA4IBAQCT8zbJ1nFfpTjPETpbQ4V2BR41Y4BZXFPcnfWY/byC8sRr
+EvbwiDWIehY/dxIHia+8dYhoXER/gWC1gc+rrW8Bil+n5119VEX9Ij6oFoQJgNrw
+rcP+W6Bjl9VTS15+A39Bo8x8ElNXRt6b91fBd77wzDamaB7UXq7eDIPu6Ov56tui
+RiYi9xXIwplWNH/XPl2HcwBw9hS5qG608qK9t/dJIpTIdWJmqg0gCxR8qmubrg58
+uvy8AhbYamLHTUa9Kf4CMxfl6QjrIMFCQiB7g6aaMZxRTQo9eRTMjoywNh8wd63g
+VYFagDTZZUGliXyIHkN6AVKvI7IIF21r7c5qarbX
-----END CERTIFICATE-----
diff --git a/spec/fixtures/clusters/intermediate_certificate.pem b/spec/fixtures/clusters/intermediate_certificate.pem
index b287201af16..142e73b99c2 100644
--- a/spec/fixtures/clusters/intermediate_certificate.pem
+++ b/spec/fixtures/clusters/intermediate_certificate.pem
@@ -1,27 +1,21 @@
-----BEGIN CERTIFICATE-----
-MIIEjzCCA3egAwIBAgIQfCoMIT/GVVNFyR8ZH7hO+jANBgkqhkiG9w0BAQsFADBM
-MSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xv
-YmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0yMjA0MjAxMjAwMDBaFw0y
-NTA0MjAwMDAwMDBaMFgxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWdu
-IG52LXNhMS4wLAYDVQQDEyVHbG9iYWxTaWduIEF0bGFzIFIzIERWIFRMUyBDQSAy
-MDIyIFEzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuKh6ZjxOZpzO
-N6VUNU02x5nTqCc28i/G1Rg+6QndBdbXLDQyfAhjSdEQN+V4XRFizm37Lz83lNuP
-ezDpXizZVT+y27mgtWA3i6QGMjVQpAmvCkX/qB+bZY7dSuBAoeNjN1iQ3XU7/A4c
-gkCYvXCxwUgUFDwES2nd1JwBpukh44IK/uSqvzSgjMvJeW4+XGpSnsTtK8Vp/lA8
-k521/y0oqGwGbJ3Fr7JZ+1l3DXR6iISk1B3UuiAGzLUeSE50IRWGdcDMWtEFz1cW
-ehMX7MJKrtUecqoiWoycgjLEEOZCbiGGaHyAIzA1072wXgopK/AUsRg32Vklw+c4
-2enULTY1ZQIDAQABo4IBXzCCAVswDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQG
-CCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW
-BBT6kTljmvutECTlvrW52qvZxEZpqzAfBgNVHSMEGDAWgBSP8Et/qC5FJK5NUPpj
-move4t0bvDB7BggrBgEFBQcBAQRvMG0wLgYIKwYBBQUHMAGGImh0dHA6Ly9vY3Nw
-Mi5nbG9iYWxzaWduLmNvbS9yb290cjMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9zZWN1
-cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0L3Jvb3QtcjMuY3J0MDYGA1UdHwQvMC0w
-K6ApoCeGJWh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20vcm9vdC1yMy5jcmwwIQYD
-VR0gBBowGDAIBgZngQwBAgEwDAYKKwYBBAGgMgoBAzANBgkqhkiG9w0BAQsFAAOC
-AQEAFDMseeU/gsZwP9pZOKe7onasYRgFaFfZDfuKRrzxqOgMcAIdxi+X7TY+nlKG
-L1xi2NVHQ5pz0Sslh59EtBTrJrwhR3QgvZ+kv7OAHU01fc25tdpV8pBQyLIXTg60
-YYgpX0RdA39XkYHQ6zCu1SrsgiDOTtKwi5UCYXPYaTT0rWMOXOQgH6l97Y7lHAS7
-Ip/HqSLKmT0Cp2foBi36BGu7SdJsmVdjbC3CYXjhILH79r/hgjk5PHvvfRqVSrJy
-2lWQru3d4nCQfBrutTJaXc/W+kXyngEMMS+JhP4xYA/97qZbhNXHGOak+UAwKRge
-/vxBtbkpBXWLYhpbIi6/5FlssA==
+MIIDgjCCAmqgAwIBAgIJAMTV0ieHng2/MA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
+BAYTAlVTMRYwFAYDVQQIDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQHDApDYWxpZm9y
+bmlhMRYwFAYDVQQKDA1HaXRMYWIgKFRlc3QpMRUwEwYDVQQDDAxUZXN0IFJvb3Qg
+Q0EwIBcNMjIxMTIwMjMzNzQyWhgPMzAyMjAzMjMyMzM3NDJaMHExCzAJBgNVBAYT
+AlVTMRYwFAYDVQQIDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQHDApDYWxpZm9ybmlh
+MRYwFAYDVQQKDA1HaXRMYWIgKFRlc3QpMR0wGwYDVQQDDBRUZXN0IEludGVybWVk
+aWF0ZSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJfazJfCLjY9
+wGUfI5MxWtyX6H/DAAiYNSfRw28M4ZBAwJcgClvwEHeRFGVOw1TwDyopMV3JIDtk
++87AGvF1Jyu+5k+XkJtOmb6HtotjelUJS1gCgzbBoqlfHTfFCYNRD/rWV1zBFkq5
+wTMeRQvy7Q5MocBzr2VHwFvBr1YkT5P8nSsQ2iFA3r7Lsv5csgj1D7yFuI6LQcWN
+3IpCORuU7144ZNuK3ChhcmrV9S2770qAgqdmSUpIYQO4xCOFJB3Zs7kHJkKXn1cz
+jDXFsbQ//ouKG8zBDzeCawNsHMVei2oXBySZYSVXaHI1fRB9MYIAdheCn5QQSyqG
+zN7mP03iQq8CAwEAAaMjMCEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AYYwDQYJKoZIhvcNAQELBQADggEBAJaXhgFrG4ijA/TinY2ApARY5lsk0ol8DKq7
+4hVDDudRo1ET5gW3VhbYIkN9r+hZeRnzf3WghC75si7WROpv2DeNKKv2FzFg4OqF
+nw154EVvEWJ8tyCTZ/8tqIBErEJUPfzuO8bZjVpN+2eSVDbo3UBLVFbsE37is7li
+eqjic+kmS6onvlG7xN9xCAwo7zGsyfceTLC9n3D8kfJd1YYV8I/l0jU1Gxf6S7g4
+2DnqNaOGTBwdioIAhaUB4amXw4NZ/NZhG3bVsNyWt99cvtLeMFTSl9lRRMT3xxCI
++TPaT30R9qRuI1Sd0IxdybSx5QG6AuZAe/5v+2+C4+8u6onpuXE=
-----END CERTIFICATE-----
diff --git a/spec/fixtures/clusters/leaf_certificate.pem b/spec/fixtures/clusters/leaf_certificate.pem
index b7b2f78a977..ff390a84967 100644
--- a/spec/fixtures/clusters/leaf_certificate.pem
+++ b/spec/fixtures/clusters/leaf_certificate.pem
@@ -1,37 +1,21 @@
-----BEGIN CERTIFICATE-----
-MIIGYjCCBUqgAwIBAgIQATfkha/xTr3pbLHVWlPq4jANBgkqhkiG9w0BAQsFADBY
-MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEuMCwGA1UE
-AxMlR2xvYmFsU2lnbiBBdGxhcyBSMyBEViBUTFMgQ0EgMjAyMiBRMzAeFw0yMjA3
-MjIxOTQyMTFaFw0yMzA4MjMxOTQyMTBaMBsxGTAXBgNVBAMMEGFib3V0LmdpdGxh
-Yi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFFFQs8EITaWo5
-0U18/mPTDLencU/7siJT/4P8oeDkemyx98wzK6vuNj/2JEZ3v1psKun5n8Pb/fHa
-somKd/4icHgC4rnxrO6zayfb+cKzVghQe12Nj75lx6RtppqTgAmSOa3Tai5niICT
-I8s3d2wsHtfEgqAavcD0/zdPIk25Ji7yfquldSthnlhQqI4Pm3OxTiyFj/V5ZhFl
-IWZLvQaENjBSDVZQDcaPdWwodfXNA8fJmqk7cTLQ9P9NgjWvva7acl+Yd6hOFzV0
-EllBl/WF1KB+YzGuHI0CQHT7sv3GW1lXeE2EqrWoSdLTOSAqm6y02DyE79d1xvG6
-XXfX5ILlAgMBAAGjggNjMIIDXzAbBgNVHREEFDASghBhYm91dC5naXRsYWIuY29t
-MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw
-HQYDVR0OBBYEFHK7MnjGDptQWjfmJ2fr3IrjxEopMFcGA1UdIARQME4wCAYGZ4EM
-AQIBMEIGCisGAQQBoDIKAQMwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly93d3cuZ2xv
-YmFsc2lnbi5jb20vcmVwb3NpdG9yeS8wDAYDVR0TAQH/BAIwADCBngYIKwYBBQUH
-AQEEgZEwgY4wQAYIKwYBBQUHMAGGNGh0dHA6Ly9vY3NwLmdsb2JhbHNpZ24uY29t
-L2NhL2dzYXRsYXNyM2R2dGxzY2EyMDIycTMwSgYIKwYBBQUHMAKGPmh0dHA6Ly9z
-ZWN1cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0L2dzYXRsYXNyM2R2dGxzY2EyMDIy
-cTMuY3J0MB8GA1UdIwQYMBaAFPqROWOa+60QJOW+tbnaq9nERmmrMEgGA1UdHwRB
-MD8wPaA7oDmGN2h0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20vY2EvZ3NhdGxhc3Iz
-ZHZ0bHNjYTIwMjJxMy5jcmwwggF9BgorBgEEAdZ5AgQCBIIBbQSCAWkBZwB1AG9T
-dqwx8DEZ2JkApFEV/3cVHBHZAsEAKQaNsgiaN9kTAAABgiduiHEAAAQDAEYwRAIg
-SYQrru/KAKfe+hUqpJmk7Fc8drkgtY3IcAurTOwbM68CIBYO9sbDspd5p7v17RQi
-QQkjdRwSjHiIgvlX0Y1JqmXjAHYArfe++nz/EMiLnT2cHj4YarRnKV3PsQwkyoWG
-NOvcgooAAAGCJ26IcQAABAMARzBFAiBc5a10annqMEH69bdEFy/Vo1gb3S3GQ993
-BCRV7ZXG4gIhAMqnsoKkU6ITwRXwE9KGjHnijJ8QrBrnK0i+JFaGe1ffAHYAs3N3
-B+GEUPhjhtYFqdwRCUp5LbFnDAuH3PADDnk2pZoAAAGCJ26I6QAABAMARzBFAiAf
-lW8Agd0DB68YA8XAbnlq7QNHw3uRMzNdS8gtRUe75gIhANTe+mt2p1ryW83P31OW
-jH3cEGJxdUNT/oDM3Fzesx94MA0GCSqGSIb3DQEBCwUAA4IBAQAfivKEmjqqOFFh
-VsX2XYkoDtreghpqMwHMCLwNk852Alr/Seyv9Ilng8cunU4NmhvEtsYVXkfE4XvB
-0QIVxkg1w7A+p7ejMjh6doLJ0aWNWIVW/DwOeP0qstF9lqvLdLDABoVn0BtYCDTH
-gjG80e2xpvPiKHGvBL+hlOIJwUuIAT3jN23sS1GoiYQGKsz0lovB09/6MGG0Qj8C
-3i9a59T9XBpwSKdpKd4u/CB6koBXD3atbBNBACuAMcFckTEtmkCFtSpqBuocJGKf
-LB4MFVaEwrd7Lc1ACC1et5FDtEI4I3/CerkRZTV+mRz5n6tB91AK3dRvjElfhiuh
-XXYRULvB
+MIIDgDCCAmigAwIBAgIJALI40QyGMz+AMA0GCSqGSIb3DQEBCwUAMHExCzAJBgNV
+BAYTAlVTMRYwFAYDVQQIDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQHDApDYWxpZm9y
+bmlhMRYwFAYDVQQKDA1HaXRMYWIgKFRlc3QpMR0wGwYDVQQDDBRUZXN0IEludGVy
+bWVkaWF0ZSBDQTAgFw0yMjExMjEwMDAzMTZaGA8zMDIyMDMyNDAwMDMxNlowazEL
+MAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEzARBgNVBAcMCkNh
+bGlmb3JuaWExFjAUBgNVBAoMDUdpdExhYiAoVGVzdCkxFzAVBgNVBAMMDlRlc3Qg
+TGVhZiBDZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxPHRlMks
+Auba0UxFoL248jfCQNWpEbRoImj/B9W5Zm2sdmJpaD01NVfR+O7eovbWozN6J+gs
+MKS5SUVDIu0E5iVBsEsQlDAF+ckPo3kvoWDAF2cLgNjpRApIiS9YCy9dZLxUl34x
+F4nXy19+53mouw/tX7kPLwnHxTVvBEpDfIttOCTlXnLu62v2LRP5lt5vFXE0B/00
+bD6t+lVl7eamvjDRAz2HoFMY+m1Popts7j4Pj3rWVWfo1RRu88NvjQLnqccFQFnd
+tDsv3YqtiWDdCbcnkIKcgZ3ckpDhTKwRjnKmub4un/OpEgn/WVR1iLCOCDS1RX1u
+j5HKhyiEzRJt1QIDAQABox8wHTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t
+MA0GCSqGSIb3DQEBCwUAA4IBAQA31V60ppRL+/YD1qlGu2uDXKn2VaK41LQxoz7R
+LCQp6qf50XhQFMBu2Mdf5cAbMdgIcAE6BQ7r7kaxO4dSXn9J3cy5Qb4msRvGy63P
+rm0G71TmAK3ihYLP19eT5g9oOHarRc3gXVtUxCAd/XVrxp1GW2vxKTGo46rKKVeU
+myU3Yn9hlu6VszvyaseaDo4Tvd6+vfdpGRo6lcJIdjcQ3Ie2bH6kF1RWXT2sgBW/
+XsDrF7O15DUE9na/7ak8tCOnxFaag7f7SLEHC+NSiMkrRry7Yc3WEFMeJTzHCXGn
+5Uwl4FKujmFkTwOPOeKyWIkDJg+PQVXnVrTvZlx+6k+Kpv2j
-----END CERTIFICATE-----
diff --git a/spec/fixtures/clusters/root_certificate.pem b/spec/fixtures/clusters/root_certificate.pem
index 8afb219058f..6d97302b2b3 100644
--- a/spec/fixtures/clusters/root_certificate.pem
+++ b/spec/fixtures/clusters/root_certificate.pem
@@ -1,21 +1,21 @@
-----BEGIN CERTIFICATE-----
-MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G
-A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp
-Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4
-MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG
-A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
-hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8
-RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT
-gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm
-KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd
-QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ
-XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw
-DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o
-LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU
-RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp
-jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK
-6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX
-mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs
-Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
-WD9f
+MIIDejCCAmKgAwIBAgIJAMMTWTnLVjZwMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
+BAYTAlVTMRYwFAYDVQQIDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQHDApDYWxpZm9y
+bmlhMRYwFAYDVQQKDA1HaXRMYWIgKFRlc3QpMRUwEwYDVQQDDAxUZXN0IFJvb3Qg
+Q0EwIBcNMjIxMTIwMjMyMzA4WhgPMzAyMjAzMjMyMzIzMDhaMGkxCzAJBgNVBAYT
+AlVTMRYwFAYDVQQIDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQHDApDYWxpZm9ybmlh
+MRYwFAYDVQQKDA1HaXRMYWIgKFRlc3QpMRUwEwYDVQQDDAxUZXN0IFJvb3QgQ0Ew
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFe5BkdGSwAL1w7kEMgMVu
+quanndVPI7Mz5YthrmJP1Qgv5TY7440PrKKbtSqx4aS2KdNKq9th7sHZ6Qe6NKbq
+G6/HZPCVO+X7ORI8yGbWD1iYBLCXmMOkXY5UsDcN5z/oKRXzHfLD0uYSQ5Fp0D9J
+O9Y0I2iz1ywsGm784xt0n7Yzz3HgjvYwU2VdLJPSzwSBPphj64E2cfM+5rGoaJd7
+Er3FodqR0YegrsMmggpK/QPIylt+TCkTgBL/+paM4KynUYusB+LzqXm0u1mYuZmt
++9Y4sKVZiNKlQFfg9XfZRpldn8pnFBMnLaUU757+mPJOuy5mLVlwAntw9qgi6UfX
+AgMBAAGjIzAhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqG
+SIb3DQEBCwUAA4IBAQCT8zbJ1nFfpTjPETpbQ4V2BR41Y4BZXFPcnfWY/byC8sRr
+EvbwiDWIehY/dxIHia+8dYhoXER/gWC1gc+rrW8Bil+n5119VEX9Ij6oFoQJgNrw
+rcP+W6Bjl9VTS15+A39Bo8x8ElNXRt6b91fBd77wzDamaB7UXq7eDIPu6Ov56tui
+RiYi9xXIwplWNH/XPl2HcwBw9hS5qG608qK9t/dJIpTIdWJmqg0gCxR8qmubrg58
+uvy8AhbYamLHTUa9Kf4CMxfl6QjrIMFCQiB7g6aaMZxRTQo9eRTMjoywNh8wd63g
+VYFagDTZZUGliXyIHkN6AVKvI7IIF21r7c5qarbX
-----END CERTIFICATE-----
diff --git a/spec/fixtures/config/redis_cluster_format_host.yml b/spec/fixtures/config/redis_cluster_format_host.yml
new file mode 100644
index 00000000000..7303db72c4e
--- /dev/null
+++ b/spec/fixtures/config/redis_cluster_format_host.yml
@@ -0,0 +1,29 @@
+# redis://[:password@]host[:port][/db-number][?option=value]
+# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
+development:
+ password: myclusterpassword
+ cluster:
+ -
+ host: development-master1
+ port: 6379
+ -
+ host: development-master2
+ port: 6379
+test:
+ password: myclusterpassword
+ cluster:
+ -
+ host: test-master1
+ port: 6379
+ -
+ host: test-master2
+ port: 6379
+production:
+ password: myclusterpassword
+ cluster:
+ -
+ host: production-master1
+ port: 6379
+ -
+ host: production-master2
+ port: 6379
diff --git a/spec/fixtures/gitlab/database/structure_example.sql b/spec/fixtures/gitlab/database/structure_example.sql
index 1ad78adea53..feebf475d2e 100644
--- a/spec/fixtures/gitlab/database/structure_example.sql
+++ b/spec/fixtures/gitlab/database/structure_example.sql
@@ -77,3 +77,17 @@ ALTER TABLE ONLY public.abuse_reports
CREATE INDEX index_abuse_reports_on_user_id ON public.abuse_reports USING btree (user_id);
+CREATE TRIGGER gitlab_schema_write_trigger_for_users BEFORE INSERT OR DELETE OR UPDATE OR TRUNCATE ON users FOR EACH STATEMENT EXECUTE FUNCTION gitlab_schema_prevent_write();
+
+CREATE FUNCTION gitlab_schema_prevent_write() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+BEGIN
+ IF COALESCE(NULLIF(current_setting(CONCAT('lock_writes.', TG_TABLE_NAME), true), ''), 'true') THEN
+ RAISE EXCEPTION 'Table: "%" is write protected within this Gitlab database.', TG_TABLE_NAME
+ USING ERRCODE = 'modifying_sql_data_not_permitted',
+ HINT = 'Make sure you are using the right database connection';
+END IF;
+RETURN NEW;
+END
+$$;
diff --git a/spec/fixtures/gitlab/database/structure_example_cleaned.sql b/spec/fixtures/gitlab/database/structure_example_cleaned.sql
index ab6af34dda7..642a310e024 100644
--- a/spec/fixtures/gitlab/database/structure_example_cleaned.sql
+++ b/spec/fixtures/gitlab/database/structure_example_cleaned.sql
@@ -24,3 +24,16 @@ ALTER TABLE ONLY abuse_reports
ADD CONSTRAINT abuse_reports_pkey PRIMARY KEY (id);
CREATE INDEX index_abuse_reports_on_user_id ON abuse_reports USING btree (user_id);
+
+CREATE FUNCTION gitlab_schema_prevent_write() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+BEGIN
+ IF COALESCE(NULLIF(current_setting(CONCAT('lock_writes.', TG_TABLE_NAME), true), ''), 'true') THEN
+ RAISE EXCEPTION 'Table: "%" is write protected within this Gitlab database.', TG_TABLE_NAME
+ USING ERRCODE = 'modifying_sql_data_not_permitted',
+ HINT = 'Make sure you are using the right database connection';
+END IF;
+RETURN NEW;
+END
+$$;
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson
index e5d39512255..2d092d4e916 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson
@@ -7,7 +7,6 @@
{"id":95,"project_id":5,"created_at":"2016-06-14T15:01:51.255Z","updated_at":"2016-06-14T15:01:51.255Z","active":false,"properties":{"api_url":"","jira_issue_transition_id":"2"},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"JiraService","category":"issue_tracker","default":false,"wiki_page_events":true}
{"id":94,"project_id":5,"created_at":"2016-06-14T15:01:51.232Z","updated_at":"2016-06-14T15:01:51.232Z","active":true,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"IrkerService","category":"common","default":false,"wiki_page_events":true}
{"id":93,"project_id":5,"created_at":"2016-06-14T15:01:51.219Z","updated_at":"2016-06-14T15:01:51.219Z","active":false,"properties":{"notify_only_broken_pipelines":true},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"pipeline_events":true,"type":"HipchatService","category":"common","default":false,"wiki_page_events":true}
-{"id":91,"project_id":5,"created_at":"2016-06-14T15:01:51.182Z","updated_at":"2016-06-14T15:01:51.182Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"FlowdockService","category":"common","default":false,"wiki_page_events":true}
{"id":90,"project_id":5,"created_at":"2016-06-14T15:01:51.166Z","updated_at":"2016-06-14T15:01:51.166Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"ExternalWikiService","category":"common","default":false,"wiki_page_events":true}
{"id":89,"project_id":5,"created_at":"2016-06-14T15:01:51.153Z","updated_at":"2016-06-14T15:01:51.153Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"EmailsOnPushService","category":"common","default":false,"wiki_page_events":true}
{"id":88,"project_id":5,"created_at":"2016-06-14T15:01:51.139Z","updated_at":"2016-06-14T15:01:51.139Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"DroneCiService","category":"ci","default":false,"wiki_page_events":true}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_full_syntax.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_full_syntax.json
index a74b557dabe..145cc476d64 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_full_syntax.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_full_syntax.json
@@ -2,12 +2,21 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
- "type", "options"
+ "type",
+ "options"
],
"properties": {
- "type": { "enum": "metric_label_values" },
- "label": { "type": "string" },
- "options": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_options.json" }
+ "type": {
+ "enum": [
+ "metric_label_values"
+ ]
+ },
+ "label": {
+ "type": "string"
+ },
+ "options": {
+ "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_options.json"
+ }
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 14885813d93..38b2a8381bb 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -399,3 +399,7 @@ Bob -> Sara : Hello
[beard]-:>[foul mouth]
]
```
+
+### Image Attributes
+
+![Sized Image](app/assets/images/touch-icon-ipad.png){width=75% height=100}
diff --git a/spec/fixtures/markdown/markdown_golden_master_examples.yml b/spec/fixtures/markdown/markdown_golden_master_examples.yml
index 0c7e6ab5cd2..9e7de030a29 100644
--- a/spec/fixtures/markdown/markdown_golden_master_examples.yml
+++ b/spec/fixtures/markdown/markdown_golden_master_examples.yml
@@ -347,6 +347,16 @@
<li data-sourcepos="9:1-9:25"><code>HSLA(540,70%,50%,0.3)<span class="gfm-color_chip"><span style="background-color: HSLA(540,70%,50%,0.3);"></span></span></code></li>
</ul>
+- name: comment
+ markdown: |-
+ <!-- this is a
+ multiline markdown
+ comment -->
+ html: |-
+ <!-- this is a
+ multiline markdown
+ comment -->
+
- name: description_list
markdown: |-
<dl>
diff --git a/spec/fixtures/pager_duty/webhook_incident_trigger.json b/spec/fixtures/pager_duty/webhook_incident_trigger.json
index 872297adcf6..9994c8c843b 100644
--- a/spec/fixtures/pager_duty/webhook_incident_trigger.json
+++ b/spec/fixtures/pager_duty/webhook_incident_trigger.json
@@ -1,239 +1,61 @@
{
- "messages": [
- {
- "event": "incident.trigger",
- "log_entries": [
+ "event": {
+ "id": "01DDZJG3TC199M1GJQ7LO67JYS",
+ "event_type": "incident.triggered",
+ "resource_type": "incident",
+ "occurred_at": "2022-11-30T08:46:19.079Z",
+ "agent": {
+ "html_url": "https://gitlab-1.pagerduty.com/users/PIN0B5C",
+ "id": "PIN0B5C",
+ "self": "https://api.pagerduty.com/users/PIN0B5C",
+ "summary": "Rajendra Kadam",
+ "type": "user_reference"
+ },
+ "client": "nil",
+ "data": {
+ "id": "Q1XZUF87W1HB5A",
+ "type": "incident",
+ "self": "https://api.pagerduty.com/incidents/Q1XZUF87W1HB5A",
+ "html_url": "https://gitlab-1.pagerduty.com/incidents/Q1XZUF87W1HB5A",
+ "number": 2,
+ "status": "triggered",
+ "incident_key": "[FILTERED]",
+ "created_at": "2022-11-30T08:46:19Z",
+ "title": "[FILTERED]",
+ "service": {
+ "html_url": "https://gitlab-1.pagerduty.com/services/PK6IKMT",
+ "id": "PK6IKMT",
+ "self": "https://api.pagerduty.com/services/PK6IKMT",
+ "summary": "Test service",
+ "type": "service_reference"
+ },
+ "assignees": [
{
- "id": "R2XGXEI3W0FHMSDXHDIBQGBQ5E",
- "type": "trigger_log_entry",
- "summary": "Triggered through the website",
- "self": "https://api.pagerduty.com/log_entries/R2XGXEI3W0FHMSDXHDIBQGBQ5E",
- "html_url": "https://webdemo.pagerduty.com/incidents/PRORDTY/log_entries/R2XGXEI3W0FHMSDXHDIBQGBQ5E",
- "created_at": "2017-09-26T15:14:36Z",
- "agent": {
- "id": "P553OPV",
- "type": "user_reference",
- "summary": "Laura Haley",
- "self": "https://api.pagerduty.com/users/P553OPV",
- "html_url": "https://webdemo.pagerduty.com/users/P553OPV"
- },
- "channel": {
- "type": "web_trigger",
- "summary": "My new incident",
- "subject": "My new incident",
- "details": "Oh my gosh",
- "details_omitted": false
- },
- "service": {
- "id": "PN49J75",
- "type": "service_reference",
- "summary": "Production XDB Cluster",
- "self": "https://api.pagerduty.com/services/PN49J75",
- "html_url": "https://webdemo.pagerduty.com/services/PN49J75"
- },
- "incident": {
- "id": "PRORDTY",
- "type": "incident_reference",
- "summary": "[#33] My new incident",
- "self": "https://api.pagerduty.com/incidents/PRORDTY",
- "html_url": "https://webdemo.pagerduty.com/incidents/PRORDTY"
- },
- "teams": [
- {
- "id": "P4SI59S",
- "type": "team_reference",
- "summary": "Engineering",
- "self": "https://api.pagerduty.com/teams/P4SI59S",
- "html_url": "https://webdemo.pagerduty.com/teams/P4SI59S"
- }
- ],
- "contexts": [],
- "event_details": {
- "description": "My new incident"
- }
+ "html_url": "https://gitlab-1.pagerduty.com/users/PIN0B5C",
+ "id": "PIN0B5C",
+ "self": "https://api.pagerduty.com/users/PIN0B5C",
+ "summary": "Rajendra Kadam",
+ "type": "user_reference"
}
],
- "webhook": {
- "endpoint_url": "https://requestb.in/18ao6fs1",
- "name": "V2 wabhook",
- "description": null,
- "webhook_object": {
- "id": "PN49J75",
- "type": "service_reference",
- "summary": "Production XDB Cluster",
- "self": "https://api.pagerduty.com/services/PN49J75",
- "html_url": "https://webdemo.pagerduty.com/services/PN49J75"
- },
- "config": {},
- "outbound_integration": {
- "id": "PJFWPEP",
- "type": "outbound_integration_reference",
- "summary": "Generic V2 Webhook",
- "self": "https://api.pagerduty.com/outbound_integrations/PJFWPEP",
- "html_url": null
- },
- "accounts_addon": null,
- "id": "PKT9NNX",
- "type": "webhook",
- "summary": "V2 wabhook",
- "self": "https://api.pagerduty.com/webhooks/PKT9NNX",
- "html_url": null
+ "escalation_policy": {
+ "html_url": "https://gitlab-1.pagerduty.com/escalation_policies/PWP6XTY",
+ "id": "PWP6XTY",
+ "self": "https://api.pagerduty.com/escalation_policies/PWP6XTY",
+ "summary": "Default",
+ "type": "escalation_policy_reference"
},
- "incident": {
- "incident_number": 33,
- "title": "My new incident",
- "description": "My new incident",
- "created_at": "2017-09-26T15:14:36Z",
- "status": "triggered",
- "pending_actions": [
- {
- "type": "escalate",
- "at": "2017-09-26T15:44:36Z"
- },
- {
- "type": "resolve",
- "at": "2017-09-26T19:14:36Z"
- }
- ],
- "incident_key": null,
- "service": {
- "id": "PN49J75",
- "name": "Production XDB Cluster",
- "description": "This service was created during onboarding on July 5, 2017.",
- "auto_resolve_timeout": 14400,
- "acknowledgement_timeout": 1800,
- "created_at": "2017-07-05T17:33:09Z",
- "status": "critical",
- "last_incident_timestamp": "2017-09-26T15:14:36Z",
- "teams": [
- {
- "id": "P4SI59S",
- "type": "team_reference",
- "summary": "Engineering",
- "self": "https://api.pagerduty.com/teams/P4SI59S",
- "html_url": "https://webdemo.pagerduty.com/teams/P4SI59S"
- }
- ],
- "incident_urgency_rule": {
- "type": "constant",
- "urgency": "high"
- },
- "scheduled_actions": [],
- "support_hours": null,
- "escalation_policy": {
- "id": "PINYWEF",
- "type": "escalation_policy_reference",
- "summary": "Default",
- "self": "https://api.pagerduty.com/escalation_policies/PINYWEF",
- "html_url": "https://webdemo.pagerduty.com/escalation_policies/PINYWEF"
- },
- "addons": [],
- "privilege": null,
- "alert_creation": "create_alerts_and_incidents",
- "integrations": [
- {
- "id": "PUAYF96",
- "type": "generic_events_api_inbound_integration_reference",
- "summary": "API",
- "self": "https://api.pagerduty.com/services/PN49J75/integrations/PUAYF96",
- "html_url": "https://webdemo.pagerduty.com/services/PN49J75/integrations/PUAYF96"
- },
- {
- "id": "P90GZUH",
- "type": "generic_email_inbound_integration_reference",
- "summary": "Email",
- "self": "https://api.pagerduty.com/services/PN49J75/integrations/P90GZUH",
- "html_url": "https://webdemo.pagerduty.com/services/PN49J75/integrations/P90GZUH"
- }
- ],
- "metadata": {},
- "type": "service",
- "summary": "Production XDB Cluster",
- "self": "https://api.pagerduty.com/services/PN49J75",
- "html_url": "https://webdemo.pagerduty.com/services/PN49J75"
- },
- "assignments": [
- {
- "at": "2017-09-26T15:14:36Z",
- "assignee": {
- "id": "P553OPV",
- "type": "user_reference",
- "summary": "Laura Haley",
- "self": "https://api.pagerduty.com/users/P553OPV",
- "html_url": "https://webdemo.pagerduty.com/users/P553OPV"
- }
- }
- ],
- "acknowledgements": [],
- "last_status_change_at": "2017-09-26T15:14:36Z",
- "last_status_change_by": {
- "id": "PN49J75",
- "type": "service_reference",
- "summary": "Production XDB Cluster",
- "self": "https://api.pagerduty.com/services/PN49J75",
- "html_url": "https://webdemo.pagerduty.com/services/PN49J75"
- },
- "first_trigger_log_entry": {
- "id": "R2XGXEI3W0FHMSDXHDIBQGBQ5E",
- "type": "trigger_log_entry_reference",
- "summary": "Triggered through the website",
- "self": "https://api.pagerduty.com/log_entries/R2XGXEI3W0FHMSDXHDIBQGBQ5E",
- "html_url": "https://webdemo.pagerduty.com/incidents/PRORDTY/log_entries/R2XGXEI3W0FHMSDXHDIBQGBQ5E"
- },
- "escalation_policy": {
- "id": "PINYWEF",
- "type": "escalation_policy_reference",
- "summary": "Default",
- "self": "https://api.pagerduty.com/escalation_policies/PINYWEF",
- "html_url": "https://webdemo.pagerduty.com/escalation_policies/PINYWEF"
- },
- "privilege": null,
- "teams": [
- {
- "id": "P4SI59S",
- "type": "team_reference",
- "summary": "Engineering",
- "self": "https://api.pagerduty.com/teams/P4SI59S",
- "html_url": "https://webdemo.pagerduty.com/teams/P4SI59S"
- }
- ],
- "alert_counts": {
- "all": 0,
- "triggered": 0,
- "resolved": 0
- },
- "impacted_services": [
- {
- "id": "PN49J75",
- "type": "service_reference",
- "summary": "Production XDB Cluster",
- "self": "https://api.pagerduty.com/services/PN49J75",
- "html_url": "https://webdemo.pagerduty.com/services/PN49J75"
- }
- ],
- "is_mergeable": true,
- "basic_alert_grouping": null,
- "alert_grouping": null,
- "metadata": {},
- "external_references": [],
- "importance": null,
- "incidents_responders": [],
- "responder_requests": [],
- "subscriber_requests": [],
- "urgency": "high",
- "id": "PRORDTY",
- "type": "incident",
- "summary": "[#33] My new incident",
- "self": "https://api.pagerduty.com/incidents/PRORDTY",
- "html_url": "https://webdemo.pagerduty.com/incidents/PRORDTY",
- "alerts": [
- {
- "alert_key": "c24117fc42e44b44b4d6876190583378"
- }
- ]
+ "teams": [],
+ "priority": {
+ "html_url": "https://gitlab-1.pagerduty.com/account/incident_priorities",
+ "id": "PKWBGFQ",
+ "self": "https://api.pagerduty.com/priorities/PKWBGFQ",
+ "summary": "P2",
+ "type": "priority_reference"
},
- "id": "69a7ced0-a2cd-11e7-a799-22000a15839c",
- "created_on": "2017-09-26T15:14:36Z"
+ "urgency": "high",
+ "conference_bridge": "nil",
+ "resolve_reason": "nil"
}
- ]
+ }
}
diff --git a/spec/frontend/__helpers__/dom_events_helper.js b/spec/frontend/__helpers__/dom_events_helper.js
deleted file mode 100644
index 865ea97903f..00000000000
--- a/spec/frontend/__helpers__/dom_events_helper.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export const triggerDOMEvent = (type) => {
- window.document.dispatchEvent(
- new Event(type, {
- bubbles: true,
- cancelable: true,
- }),
- );
-};
diff --git a/spec/frontend/__helpers__/filtered_search_spec_helper.js b/spec/frontend/__helpers__/filtered_search_spec_helper.js
index ecf10694a16..f76fdfca229 100644
--- a/spec/frontend/__helpers__/filtered_search_spec_helper.js
+++ b/spec/frontend/__helpers__/filtered_search_spec_helper.js
@@ -1,3 +1,5 @@
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+
export default class FilteredSearchSpecHelper {
static createFilterVisualTokenHTML(name, operator, value, isSelected) {
return FilteredSearchSpecHelper.createFilterVisualToken(name, operator, value, isSelected)
@@ -43,7 +45,7 @@ export default class FilteredSearchSpecHelper {
static createSearchVisualToken(name) {
const li = document.createElement('li');
- li.classList.add('js-visual-token', 'filtered-search-term');
+ li.classList.add('js-visual-token', FILTERED_SEARCH_TERM);
li.innerHTML = `<div class="name">${name}</div>`;
return li;
}
diff --git a/spec/frontend/__helpers__/graphql_helpers.js b/spec/frontend/__helpers__/graphql_helpers.js
deleted file mode 100644
index 63123aa046f..00000000000
--- a/spec/frontend/__helpers__/graphql_helpers.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * Returns a clone of the given object with all __typename keys omitted,
- * including deeply nested ones.
- *
- * Only works with JSON-serializable objects.
- *
- * @param {object} An object with __typename keys (e.g., a GraphQL response)
- * @returns {object} A new object with no __typename keys
- */
-export const stripTypenames = (object) => {
- return JSON.parse(
- JSON.stringify(object, (key, value) => (key === '__typename' ? undefined : value)),
- );
-};
diff --git a/spec/frontend/__helpers__/graphql_helpers_spec.js b/spec/frontend/__helpers__/graphql_helpers_spec.js
deleted file mode 100644
index dd23fbbf4e9..00000000000
--- a/spec/frontend/__helpers__/graphql_helpers_spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { stripTypenames } from './graphql_helpers';
-
-describe('stripTypenames', () => {
- it.each`
- input | expected
- ${{}} | ${{}}
- ${{ __typename: 'Foo' }} | ${{}}
- ${{ bar: 'bar', __typename: 'Foo' }} | ${{ bar: 'bar' }}
- ${{ bar: { __typename: 'Bar' }, __typename: 'Foo' }} | ${{ bar: {} }}
- ${{ bar: [{ __typename: 'Bar' }], __typename: 'Foo' }} | ${{ bar: [{}] }}
- ${[]} | ${[]}
- ${[{ __typename: 'Foo' }]} | ${[{}]}
- ${[{ bar: [{ a: 1, __typename: 'Bar' }] }]} | ${[{ bar: [{ a: 1 }] }]}
- `('given $input returns $expected, with all __typename keys removed', ({ input, expected }) => {
- const actual = stripTypenames(input);
- expect(actual).toEqual(expected);
- expect(input).not.toBe(actual);
- });
-
- it('given null returns null', () => {
- expect(stripTypenames(null)).toEqual(null);
- });
-});
diff --git a/spec/frontend/__helpers__/graphql_transformer.js b/spec/frontend/__helpers__/graphql_transformer.js
index e776e2ea6ac..f26b63dadfd 100644
--- a/spec/frontend/__helpers__/graphql_transformer.js
+++ b/spec/frontend/__helpers__/graphql_transformer.js
@@ -3,6 +3,8 @@ const loader = require('graphql-tag/loader');
module.exports = {
process(src) {
- return loader.call({ cacheable() {} }, src);
+ return {
+ code: loader.call({ cacheable() {} }, src),
+ };
},
};
diff --git a/spec/frontend/__helpers__/jest_helpers.js b/spec/frontend/__helpers__/jest_helpers.js
deleted file mode 100644
index 273d2c91966..00000000000
--- a/spec/frontend/__helpers__/jest_helpers.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
-@module
-
-This method provides convenience functions to help migrating from Karma/Jasmine to Jest.
-
-Try not to use these in new tests - this module is provided primarily for convenience of migrating tests.
- */
-
-/**
- * Creates a plain JS object pre-populated with Jest spy functions. Useful for making simple mocks classes.
- *
- * @see https://jasmine.github.io/2.0/introduction.html#section-Spies:_%3Ccode%3EcreateSpyObj%3C/code%3E
- * @param {string} baseName Human-readable name of the object. This is used for reporting purposes.
- * @param methods {string[]} List of method names that will be added to the spy object.
- */
-export function createSpyObj(baseName, methods) {
- const obj = {};
- methods.forEach((method) => {
- obj[method] = jest.fn().mockName(`${baseName}#${method}`);
- });
- return obj;
-}
diff --git a/spec/frontend/__helpers__/mock_window_location_helper.js b/spec/frontend/__helpers__/mock_window_location_helper.js
index 14082857053..de1e8c99b54 100644
--- a/spec/frontend/__helpers__/mock_window_location_helper.js
+++ b/spec/frontend/__helpers__/mock_window_location_helper.js
@@ -21,18 +21,31 @@ const useMockLocation = (fn) => {
afterEach(() => {
currentWindowLocation = origWindowLocation;
});
+
+ return () => {
+ beforeEach(() => {
+ currentWindowLocation = origWindowLocation;
+ });
+ };
};
/**
* Create an object with the location interface but `jest.fn()` implementations.
*/
export const createWindowLocationSpy = () => {
- return {
+ const { origin, href } = window.location;
+
+ const mockLocation = {
assign: jest.fn(),
reload: jest.fn(),
replace: jest.fn(),
toString: jest.fn(),
+ origin,
+ // TODO: Do we need to update `origin` if `href` is changed?
+ href,
};
+
+ return mockLocation;
};
/**
diff --git a/spec/frontend/__helpers__/raw_transformer.js b/spec/frontend/__helpers__/raw_transformer.js
index 09101b7a64f..3b0bed14e8d 100644
--- a/spec/frontend/__helpers__/raw_transformer.js
+++ b/spec/frontend/__helpers__/raw_transformer.js
@@ -1,6 +1,6 @@
/* eslint-disable import/no-commonjs */
module.exports = {
process: (content) => {
- return `module.exports = ${JSON.stringify(content)}`;
+ return { code: `module.exports = ${JSON.stringify(content)}` };
},
};
diff --git a/spec/frontend/__helpers__/set_timeout_promise_helper.js b/spec/frontend/__helpers__/set_timeout_promise_helper.js
deleted file mode 100644
index afd18d92d15..00000000000
--- a/spec/frontend/__helpers__/set_timeout_promise_helper.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export default (time = 0) =>
- new Promise((resolve) => {
- setTimeout(resolve, time);
- });
diff --git a/spec/frontend/__helpers__/web_worker_transformer.js b/spec/frontend/__helpers__/web_worker_transformer.js
index 767ab3f5675..86be856f7b7 100644
--- a/spec/frontend/__helpers__/web_worker_transformer.js
+++ b/spec/frontend/__helpers__/web_worker_transformer.js
@@ -1,18 +1,22 @@
/* eslint-disable import/no-commonjs */
-const babelJestTransformer = require('babel-jest');
+const { createTransformer } = require('babel-jest');
// This Jest will transform the code of a WebWorker module into a FakeWebWorker subclass.
// This is meant to mirror Webpack's [`worker-loader`][1].
// [1]: https://webpack.js.org/loaders/worker-loader/
module.exports = {
process: (contentArg, filename, ...args) => {
- const { code: content } = babelJestTransformer.default.process(contentArg, filename, ...args);
+ const { code: content } = createTransformer().process(contentArg, filename, ...args);
- return `const { FakeWebWorker } = require("helpers/web_worker_fake");
+ const jestTransformedWorkerCode = `const { FakeWebWorker } = require("helpers/web_worker_fake");
module.exports = class JestTransformedWorker extends FakeWebWorker {
constructor() {
super(${JSON.stringify(filename)}, ${JSON.stringify(content)});
}
};`;
+
+ return {
+ code: jestTransformedWorkerCode,
+ };
},
};
diff --git a/spec/frontend/__helpers__/yaml_transformer.js b/spec/frontend/__helpers__/yaml_transformer.js
index a23f9b1f715..e0b4d01f542 100644
--- a/spec/frontend/__helpers__/yaml_transformer.js
+++ b/spec/frontend/__helpers__/yaml_transformer.js
@@ -6,6 +6,6 @@ module.exports = {
process: (sourceContent) => {
const jsonContent = JsYaml.load(sourceContent);
const json = JSON.stringify(jsonContent);
- return `module.exports = ${json}`;
+ return { code: `module.exports = ${json}` };
},
};
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index 6f2888e5c42..4d893bcd0bd 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -49,6 +49,8 @@ jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
'boundary',
'container',
'showCloseButton',
+ 'show',
+ 'boundaryPadding',
].map((prop) => [prop, {}]),
),
},
diff --git a/spec/frontend/admin/background_migrations/components/database_listbox_spec.js b/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
index 3778943872e..212f4c0842c 100644
--- a/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
+++ b/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
@@ -1,4 +1,4 @@
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import BackgroundMigrationsDatabaseListbox from '~/admin/background_migrations/components/database_listbox.vue';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
@@ -30,15 +30,15 @@ describe('BackgroundMigrationsDatabaseListbox', () => {
wrapper.destroy();
});
- const findGlListbox = () => wrapper.findComponent(GlListbox);
+ const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
describe('template always', () => {
beforeEach(() => {
createComponent();
});
- it('renders GlListbox', () => {
- expect(findGlListbox().exists()).toBe(true);
+ it('renders GlCollapsibleListbox', () => {
+ expect(findGlCollapsibleListbox().exists()).toBe(true);
});
});
@@ -48,7 +48,7 @@ describe('BackgroundMigrationsDatabaseListbox', () => {
});
it('selecting a listbox item fires visitUrl with the database param', () => {
- findGlListbox().vm.$emit('select', MOCK_DATABASES[1].value);
+ findGlCollapsibleListbox().vm.$emit('select', MOCK_DATABASES[1].value);
expect(setUrlParams).toHaveBeenCalledWith({ database: MOCK_DATABASES[1].value });
expect(visitUrl).toHaveBeenCalled();
diff --git a/spec/frontend/admin/broadcast_messages/components/datetime_picker_spec.js b/spec/frontend/admin/broadcast_messages/components/datetime_picker_spec.js
new file mode 100644
index 00000000000..291c3aed1cf
--- /dev/null
+++ b/spec/frontend/admin/broadcast_messages/components/datetime_picker_spec.js
@@ -0,0 +1,46 @@
+import { mount } from '@vue/test-utils';
+import { GlDatepicker } from '@gitlab/ui';
+import DatetimePicker from '~/admin/broadcast_messages/components/datetime_picker.vue';
+
+describe('DatetimePicker', () => {
+ let wrapper;
+
+ const toDate = (day, time) => new Date(`${day}T${time}:00.000Z`);
+ const findDatepicker = () => wrapper.findComponent(GlDatepicker);
+ const findTimepicker = () => wrapper.findComponent('[data-testid="time-picker"]');
+
+ const testDay = '2022-03-22';
+ const testTime = '01:23';
+
+ function createComponent() {
+ wrapper = mount(DatetimePicker, {
+ propsData: {
+ value: toDate(testDay, testTime),
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the Date in the datepicker and timepicker inputs', () => {
+ expect(findDatepicker().props().value).toEqual(toDate(testDay, testTime));
+ expect(findTimepicker().element.value).toEqual(testTime);
+ });
+
+ it('emits Date with the new day/old time when the date picker changes', () => {
+ const newDay = '1992-06-30';
+ const newTime = '08:00';
+
+ findDatepicker().vm.$emit('input', toDate(newDay, newTime));
+ expect(wrapper.emitted().input).toEqual([[toDate(newDay, testTime)]]);
+ });
+
+ it('emits Date with the old day/new time when the time picker changes', () => {
+ const newTime = '08:00';
+
+ findTimepicker().vm.$emit('input', newTime);
+ expect(wrapper.emitted().input).toEqual([[toDate(testDay, newTime)]]);
+ });
+});
diff --git a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
new file mode 100644
index 00000000000..88ea79f38b3
--- /dev/null
+++ b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
@@ -0,0 +1,201 @@
+import { mount } from '@vue/test-utils';
+import { GlBroadcastMessage, GlForm } from '@gitlab/ui';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { createAlert } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
+import MessageForm from '~/admin/broadcast_messages/components/message_form.vue';
+import {
+ BROADCAST_MESSAGES_PATH,
+ TYPE_BANNER,
+ TYPE_NOTIFICATION,
+ THEMES,
+} from '~/admin/broadcast_messages/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { MOCK_TARGET_ACCESS_LEVELS } from '../mock_data';
+
+jest.mock('~/flash');
+
+describe('MessageForm', () => {
+ let wrapper;
+ let axiosMock;
+
+ const defaultProps = {
+ message: 'zzzzzzz',
+ broadcastType: TYPE_BANNER,
+ theme: THEMES[0].value,
+ dismissable: false,
+ targetPath: '',
+ targetAccessLevels: [],
+ startsAt: new Date(),
+ endsAt: new Date(),
+ };
+
+ const findPreview = () => extendedWrapper(wrapper.findComponent(GlBroadcastMessage));
+ const findThemeSelect = () => wrapper.findComponent('[data-testid=theme-select]');
+ const findDismissable = () => wrapper.findComponent('[data-testid=dismissable-checkbox]');
+ const findTargetRoles = () => wrapper.findComponent('[data-testid=target-roles-checkboxes]');
+ const findSubmitButton = () => wrapper.findComponent('[data-testid=submit-button]');
+ const findForm = () => wrapper.findComponent(GlForm);
+
+ function createComponent({ broadcastMessage = {}, glFeatures = {} }) {
+ wrapper = mount(MessageForm, {
+ provide: {
+ glFeatures,
+ targetAccessLevelOptions: MOCK_TARGET_ACCESS_LEVELS,
+ },
+ propsData: {
+ broadcastMessage: {
+ ...defaultProps,
+ ...broadcastMessage,
+ },
+ },
+ });
+ }
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ createAlert.mockClear();
+ });
+
+ describe('the message preview', () => {
+ it('renders the preview with the user selected theme', () => {
+ const theme = 'blue';
+ createComponent({ broadcastMessage: { theme } });
+ expect(findPreview().props().theme).toEqual(theme);
+ });
+
+ it('renders the placeholder text when the user message is blank', () => {
+ createComponent({ broadcastMessage: { message: ' ' } });
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.messagePlaceholder);
+ });
+ });
+
+ describe('theme select dropdown', () => {
+ it('renders for Banners', () => {
+ createComponent({ broadcastMessage: { broadcastType: TYPE_BANNER } });
+ expect(findThemeSelect().exists()).toBe(true);
+ });
+
+ it('does not render for Notifications', () => {
+ createComponent({ broadcastMessage: { broadcastType: TYPE_NOTIFICATION } });
+ expect(findThemeSelect().exists()).toBe(false);
+ });
+ });
+
+ describe('dismissable checkbox', () => {
+ it('renders for Banners', () => {
+ createComponent({ broadcastMessage: { broadcastType: TYPE_BANNER } });
+ expect(findDismissable().exists()).toBe(true);
+ });
+
+ it('does not render for Notifications', () => {
+ createComponent({ broadcastMessage: { broadcastType: TYPE_NOTIFICATION } });
+ expect(findDismissable().exists()).toBe(false);
+ });
+ });
+
+ describe('target roles checkboxes', () => {
+ it('renders when roleTargetedBroadcastMessages feature is enabled', () => {
+ createComponent({ glFeatures: { roleTargetedBroadcastMessages: true } });
+ expect(findTargetRoles().exists()).toBe(true);
+ });
+
+ it('does not render when roleTargetedBroadcastMessages feature is disabled', () => {
+ createComponent({ glFeatures: { roleTargetedBroadcastMessages: false } });
+ expect(findTargetRoles().exists()).toBe(false);
+ });
+ });
+
+ describe('form submit button', () => {
+ it('renders the "add" text when the message is not persisted', () => {
+ createComponent({ broadcastMessage: { id: undefined } });
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.add);
+ });
+
+ it('renders the "update" text when the message is persisted', () => {
+ createComponent({ broadcastMessage: { id: 100 } });
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.update);
+ });
+
+ it('is disabled when the user message is blank', () => {
+ createComponent({ broadcastMessage: { message: ' ' } });
+ expect(findSubmitButton().props().disabled).toBe(true);
+ });
+
+ it('is not disabled when the user message is present', () => {
+ createComponent({ broadcastMessage: { message: 'alsdjfkldsj' } });
+ expect(findSubmitButton().props().disabled).toBe(false);
+ });
+ });
+
+ describe('form submission', () => {
+ const defaultPayload = {
+ message: defaultProps.message,
+ broadcast_type: defaultProps.broadcastType,
+ theme: defaultProps.theme,
+ dismissable: defaultProps.dismissable,
+ target_path: defaultProps.targetPath,
+ target_access_levels: defaultProps.targetAccessLevels,
+ starts_at: defaultProps.startsAt,
+ ends_at: defaultProps.endsAt,
+ };
+
+ it('sends a create request for a new message form', async () => {
+ createComponent({ broadcastMessage: { id: undefined } });
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(axiosMock.history.post).toHaveLength(1);
+ expect(axiosMock.history.post[0]).toMatchObject({
+ url: BROADCAST_MESSAGES_PATH,
+ data: JSON.stringify(defaultPayload),
+ });
+ });
+
+ it('shows an error alert if the create request fails', async () => {
+ createComponent({ broadcastMessage: { id: undefined } });
+ axiosMock.onPost(BROADCAST_MESSAGES_PATH).replyOnce(httpStatus.BAD_REQUEST);
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: wrapper.vm.$options.i18n.addError,
+ }),
+ );
+ });
+
+ it('sends an update request for a persisted message form', async () => {
+ const id = 1337;
+ createComponent({ broadcastMessage: { id } });
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(axiosMock.history.patch).toHaveLength(1);
+ expect(axiosMock.history.patch[0]).toMatchObject({
+ url: `${BROADCAST_MESSAGES_PATH}/${id}`,
+ data: JSON.stringify(defaultPayload),
+ });
+ });
+
+ it('shows an error alert if the update request fails', async () => {
+ const id = 1337;
+ createComponent({ broadcastMessage: { id } });
+ axiosMock.onPost(`${BROADCAST_MESSAGES_PATH}/${id}`).replyOnce(httpStatus.BAD_REQUEST);
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: wrapper.vm.$options.i18n.updateError,
+ }),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/admin/broadcast_messages/mock_data.js b/spec/frontend/admin/broadcast_messages/mock_data.js
index 8dd98c2319d..2e20b5cf638 100644
--- a/spec/frontend/admin/broadcast_messages/mock_data.js
+++ b/spec/frontend/admin/broadcast_messages/mock_data.js
@@ -15,3 +15,11 @@ export const generateMockMessages = (n) =>
[...Array(n).keys()].map((id) => generateMockMessage(id + 1));
export const MOCK_MESSAGES = generateMockMessages(5).map((id) => generateMockMessage(id));
+
+export const MOCK_TARGET_ACCESS_LEVELS = [
+ ['Guest', 10],
+ ['Reporter', 20],
+ ['Developer', 30],
+ ['Maintainer', 40],
+ ['Owner', 50],
+];
diff --git a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
index e6718f62b91..f2a951bcc76 100644
--- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
+++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
@@ -54,7 +54,6 @@ describe('Signup Form', () => {
prop | propValue | elementSelector | formElementPassedDataType | formElementKey | expected
${'signupEnabled'} | ${mockData.signupEnabled} | ${'[name="application_setting[signup_enabled]"]'} | ${'prop'} | ${'value'} | ${mockData.signupEnabled}
${'requireAdminApprovalAfterUserSignup'} | ${mockData.requireAdminApprovalAfterUserSignup} | ${'[name="application_setting[require_admin_approval_after_user_signup]"]'} | ${'prop'} | ${'value'} | ${mockData.requireAdminApprovalAfterUserSignup}
- ${'sendUserConfirmationEmail'} | ${mockData.sendUserConfirmationEmail} | ${'[name="application_setting[send_user_confirmation_email]"]'} | ${'prop'} | ${'value'} | ${mockData.sendUserConfirmationEmail}
${'newUserSignupsCap'} | ${mockData.newUserSignupsCap} | ${'[name="application_setting[new_user_signups_cap]"]'} | ${'attribute'} | ${'value'} | ${mockData.newUserSignupsCap}
${'minimumPasswordLength'} | ${mockData.minimumPasswordLength} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'value'} | ${mockData.minimumPasswordLength}
${'minimumPasswordLengthMin'} | ${mockData.minimumPasswordLengthMin} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'min'} | ${mockData.minimumPasswordLengthMin}
diff --git a/spec/frontend/admin/signup_restrictions/mock_data.js b/spec/frontend/admin/signup_restrictions/mock_data.js
index dd1ed317497..ce5ec2248fe 100644
--- a/spec/frontend/admin/signup_restrictions/mock_data.js
+++ b/spec/frontend/admin/signup_restrictions/mock_data.js
@@ -3,7 +3,6 @@ export const rawMockData = {
settingsPath: 'path/to/settings',
signupEnabled: 'true',
requireAdminApprovalAfterUserSignup: 'true',
- sendUserConfirmationEmail: 'true',
emailConfirmationSetting: 'hard',
minimumPasswordLength: '8',
minimumPasswordLengthMin: '3',
@@ -32,7 +31,6 @@ export const mockData = {
settingsPath: 'path/to/settings',
signupEnabled: true,
requireAdminApprovalAfterUserSignup: true,
- sendUserConfirmationEmail: true,
emailConfirmationSetting: 'hard',
minimumPasswordLength: '8',
minimumPasswordLengthMin: '3',
diff --git a/spec/frontend/admin/signup_restrictions/utils_spec.js b/spec/frontend/admin/signup_restrictions/utils_spec.js
index f07e14430f9..e393b07baa9 100644
--- a/spec/frontend/admin/signup_restrictions/utils_spec.js
+++ b/spec/frontend/admin/signup_restrictions/utils_spec.js
@@ -10,7 +10,6 @@ describe('utils', () => {
booleanAttributes: [
'signupEnabled',
'requireAdminApprovalAfterUserSignup',
- 'sendUserConfirmationEmail',
'domainDenylistEnabled',
'denylistTypeRawSelected',
'emailRestrictionsEnabled',
diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
index 4693d5a47e4..bff4905a12c 100644
--- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
+++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
@@ -16,7 +16,7 @@ exports[`Alert integration settings form default state should match the default
>
<gl-form-checkbox-stub
checked="true"
- data-qa-selector="create_issue_checkbox"
+ data-qa-selector="create_incident_checkbox"
id="2"
>
<span>
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index fcefcb7cf66..62a3e07186a 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -32,7 +32,7 @@ import {
} from '~/alerts_settings/utils/error_messages';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import httpStatusCodes, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import {
createHttpVariables,
updateHttpVariables,
@@ -358,7 +358,7 @@ describe('AlertsSettingsWrapper', () => {
});
it('shows an error alert when integration test payload is invalid', async () => {
- mock.onPost(/(.*)/).replyOnce(httpStatusCodes.UNPROCESSABLE_ENTITY);
+ mock.onPost(/(.*)/).replyOnce(HTTP_STATUS_UNPROCESSABLE_ENTITY);
await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' });
expect(createAlert).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
expect(createAlert).toHaveBeenCalledTimes(1);
diff --git a/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap b/spec/frontend/analytics/cycle_analytics/__snapshots__/total_time_spec.js.snap
index 92927ef16ec..92927ef16ec 100644
--- a/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap
+++ b/spec/frontend/analytics/cycle_analytics/__snapshots__/total_time_spec.js.snap
diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/analytics/cycle_analytics/base_spec.js
index 013bea671a8..58588ff49ce 100644
--- a/spec/frontend/cycle_analytics/base_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/base_spec.js
@@ -4,12 +4,12 @@ import Vue from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
-import BaseComponent from '~/cycle_analytics/components/base.vue';
-import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
-import StageTable from '~/cycle_analytics/components/stage_table.vue';
-import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
-import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants';
-import initState from '~/cycle_analytics/store/state';
+import BaseComponent from '~/analytics/cycle_analytics/components/base.vue';
+import PathNavigation from '~/analytics/cycle_analytics/components/path_navigation.vue';
+import StageTable from '~/analytics/cycle_analytics/components/stage_table.vue';
+import ValueStreamFilters from '~/analytics/cycle_analytics/components/value_stream_filters.vue';
+import { NOT_ENOUGH_DATA_ERROR } from '~/analytics/cycle_analytics/constants';
+import initState from '~/analytics/cycle_analytics/store/state';
import {
transformedProjectStagePathData,
selectedStage,
diff --git a/spec/frontend/cycle_analytics/filter_bar_spec.js b/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js
index 36933790cf7..2b26b202882 100644
--- a/spec/frontend/cycle_analytics/filter_bar_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js
@@ -7,10 +7,16 @@ import {
filterMilestones,
filterLabels,
} from 'jest/vue_shared/components/filtered_search_bar/store/modules/filters/mock_data';
-import FilterBar from '~/cycle_analytics/components/filter_bar.vue';
-import storeConfig from '~/cycle_analytics/store';
+import FilterBar from '~/analytics/cycle_analytics/components/filter_bar.vue';
+import storeConfig from '~/analytics/cycle_analytics/store';
import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility';
+import {
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import * as utils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import initialFiltersState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state';
@@ -18,10 +24,10 @@ import UrlSync from '~/vue_shared/components/url_sync.vue';
Vue.use(Vuex);
-const milestoneTokenType = 'milestone';
-const labelsTokenType = 'labels';
-const authorTokenType = 'author';
-const assigneesTokenType = 'assignees';
+const milestoneTokenType = TOKEN_TYPE_MILESTONE;
+const labelsTokenType = TOKEN_TYPE_LABEL;
+const authorTokenType = TOKEN_TYPE_AUTHOR;
+const assigneesTokenType = TOKEN_TYPE_ASSIGNEE;
const initialFilterBarState = {
selectedMilestone: null,
@@ -162,8 +168,8 @@ describe('Filter bar', () => {
it('clicks on the search button, setFilters is dispatched', () => {
const filters = [
- { type: 'milestone', value: { data: selectedMilestone[0].title, operator: '=' } },
- { type: 'labels', value: { data: selectedLabelList[0].title, operator: '=' } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: selectedMilestone[0].title, operator: '=' } },
+ { type: TOKEN_TYPE_LABEL, value: { data: selectedLabelList[0].title, operator: '=' } },
];
findFilteredSearch().vm.$emit('onFilter', filters);
diff --git a/spec/frontend/cycle_analytics/formatted_stage_count_spec.js b/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js
index 1228b8511ea..9be92bb92bc 100644
--- a/spec/frontend/cycle_analytics/formatted_stage_count_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import Component from '~/cycle_analytics/components/formatted_stage_count.vue';
+import Component from '~/analytics/cycle_analytics/components/formatted_stage_count.vue';
describe('Formatted Stage Count', () => {
let wrapper = null;
diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/analytics/cycle_analytics/mock_data.js
index 02666260cdb..f820f755400 100644
--- a/spec/frontend/cycle_analytics/mock_data.js
+++ b/spec/frontend/analytics/cycle_analytics/mock_data.js
@@ -12,7 +12,7 @@ import {
PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC,
PAGINATION_SORT_FIELD_END_EVENT,
-} from '~/cycle_analytics/constants';
+} from '~/analytics/cycle_analytics/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime_utility';
diff --git a/spec/frontend/cycle_analytics/path_navigation_spec.js b/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js
index fec1526359c..107e62035c3 100644
--- a/spec/frontend/cycle_analytics/path_navigation_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js
@@ -2,7 +2,7 @@ import { GlPath, GlSkeletonLoader } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import Component from '~/cycle_analytics/components/path_navigation.vue';
+import Component from '~/analytics/cycle_analytics/components/path_navigation.vue';
import { transformedProjectStagePathData, selectedStage } from './mock_data';
describe('Project PathNavigation', () => {
diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/analytics/cycle_analytics/stage_table_spec.js
index 473e1d5b664..cfccce7eae9 100644
--- a/spec/frontend/cycle_analytics/stage_table_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/stage_table_spec.js
@@ -3,8 +3,8 @@ import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import StageTable from '~/cycle_analytics/components/stage_table.vue';
-import { PAGINATION_SORT_FIELD_DURATION } from '~/cycle_analytics/constants';
+import StageTable from '~/analytics/cycle_analytics/components/stage_table.vue';
+import { PAGINATION_SORT_FIELD_DURATION } from '~/analytics/cycle_analytics/constants';
import { issueEvents, issueStage, reviewStage, reviewEvents } from './mock_data';
let wrapper = null;
diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
index 94b6de85a5c..f87807804c9 100644
--- a/spec/frontend/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
@@ -1,8 +1,8 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import * as actions from '~/cycle_analytics/store/actions';
-import * as getters from '~/cycle_analytics/store/getters';
+import * as actions from '~/analytics/cycle_analytics/store/actions';
+import * as getters from '~/analytics/cycle_analytics/store/getters';
import httpStatusCodes from '~/lib/utils/http_status';
import {
allowedStages,
diff --git a/spec/frontend/cycle_analytics/store/getters_spec.js b/spec/frontend/analytics/cycle_analytics/store/getters_spec.js
index c9208045a68..8ad1e1b27de 100644
--- a/spec/frontend/cycle_analytics/store/getters_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/getters_spec.js
@@ -1,4 +1,4 @@
-import * as getters from '~/cycle_analytics/store/getters';
+import * as getters from '~/analytics/cycle_analytics/store/getters';
import {
allowedStages,
diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
index 2e9e5d91471..567fac81e1f 100644
--- a/spec/frontend/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
@@ -1,10 +1,10 @@
import { useFakeDate } from 'helpers/fake_date';
-import * as types from '~/cycle_analytics/store/mutation_types';
-import mutations from '~/cycle_analytics/store/mutations';
+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_DIRECTION_DESC,
-} from '~/cycle_analytics/constants';
+} from '~/analytics/cycle_analytics/constants';
import {
selectedStage,
rawIssueEvents,
diff --git a/spec/frontend/cycle_analytics/total_time_spec.js b/spec/frontend/analytics/cycle_analytics/total_time_spec.js
index 8cf9feab6e9..47ee7aad8c4 100644
--- a/spec/frontend/cycle_analytics/total_time_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/total_time_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import TotalTime from '~/cycle_analytics/components/total_time.vue';
+import TotalTime from '~/analytics/cycle_analytics/components/total_time.vue';
describe('TotalTime', () => {
let wrapper = null;
diff --git a/spec/frontend/cycle_analytics/utils_spec.js b/spec/frontend/analytics/cycle_analytics/utils_spec.js
index 51405a1ba4d..fe412bf7498 100644
--- a/spec/frontend/cycle_analytics/utils_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/utils_spec.js
@@ -4,7 +4,7 @@ import {
formatMedianValues,
filterStagesByHiddenStatus,
buildCycleAnalyticsInitialData,
-} from '~/cycle_analytics/utils';
+} from '~/analytics/cycle_analytics/utils';
import {
selectedStage,
allowedStages,
diff --git a/spec/frontend/cycle_analytics/value_stream_filters_spec.js b/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js
index 6e96a6d756a..4f333e95d89 100644
--- a/spec/frontend/cycle_analytics/value_stream_filters_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import Daterange from '~/analytics/shared/components/daterange.vue';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
-import FilterBar from '~/cycle_analytics/components/filter_bar.vue';
-import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
+import FilterBar from '~/analytics/cycle_analytics/components/filter_bar.vue';
+import ValueStreamFilters from '~/analytics/cycle_analytics/components/value_stream_filters.vue';
import {
createdAfter as startDate,
createdBefore as endDate,
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js
index 948dc5c9be2..948dc5c9be2 100644
--- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 1f92010b771..5209d9c2d2c 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -1,7 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, {
+ HTTP_STATUS_ACCEPTED,
+ HTTP_STATUS_CREATED,
+ HTTP_STATUS_NO_CONTENT,
+} from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -1069,7 +1073,7 @@ describe('Api', () => {
describe('when the release is successfully created', () => {
it('resolves the Promise', () => {
- mock.onPost(expectedUrl, release).replyOnce(httpStatus.CREATED);
+ mock.onPost(expectedUrl, release).replyOnce(HTTP_STATUS_CREATED);
return Api.createRelease(dummyProjectPath, release).then(() => {
expect(mock.history.post).toHaveLength(1);
@@ -1125,7 +1129,7 @@ describe('Api', () => {
describe('when the Release is successfully created', () => {
it('resolves the Promise', () => {
- mock.onPost(expectedUrl, expectedLink).replyOnce(httpStatus.CREATED);
+ mock.onPost(expectedUrl, expectedLink).replyOnce(HTTP_STATUS_CREATED);
return Api.createReleaseLink(dummyProjectPath, dummyTagName, expectedLink).then(() => {
expect(mock.history.post).toHaveLength(1);
@@ -1224,7 +1228,7 @@ describe('Api', () => {
describe('when the merge request is successfully created', () => {
it('resolves the Promise', () => {
- mock.onPost(expectedUrl, options).replyOnce(httpStatus.CREATED);
+ mock.onPost(expectedUrl, options).replyOnce(HTTP_STATUS_CREATED);
return Api.createProjectMergeRequest(dummyProjectPath, options).then(() => {
expect(mock.history.post).toHaveLength(1);
@@ -1332,7 +1336,7 @@ describe('Api', () => {
describe('when the freeze period is successfully created', () => {
it('resolves the Promise', () => {
- mock.onPost(expectedUrl, options).replyOnce(httpStatus.CREATED, expectedResult);
+ mock.onPost(expectedUrl, options).replyOnce(HTTP_STATUS_CREATED, expectedResult);
return Api.createFreezePeriod(projectId, options).then(({ data }) => {
expect(data).toStrictEqual(expectedResult);
@@ -1598,7 +1602,7 @@ describe('Api', () => {
const secureFileId = 2;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/secure_files/${secureFileId}`;
- mock.onDelete(expectedUrl).reply(httpStatus.NO_CONTENT, '');
+ mock.onDelete(expectedUrl).reply(HTTP_STATUS_NO_CONTENT, '');
const { data } = await Api.deleteProjectSecureFile(projectId, secureFileId);
expect(data).toEqual('');
});
@@ -1609,10 +1613,10 @@ describe('Api', () => {
const groupId = 1;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/dependency_proxy/cache`;
- mock.onDelete(expectedUrl).reply(httpStatus.ACCEPTED);
+ mock.onDelete(expectedUrl).reply(HTTP_STATUS_ACCEPTED);
const { status } = await Api.deleteDependencyProxyCacheList(groupId, {});
- expect(status).toBe(httpStatus.ACCEPTED);
+ expect(status).toBe(HTTP_STATUS_ACCEPTED);
});
});
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index 03ecbc01a56..2dfcdd551a1 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -1,19 +1,20 @@
import { nextTick } from 'vue';
import { GlButton, GlBadge } from '@gitlab/ui';
-import { getByRole } from '@testing-library/dom';
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import PublishButton from '~/batch_comments/components/publish_button.vue';
import { createStore } from '~/batch_comments/stores';
import NoteableNote from '~/notes/components/noteable_note.vue';
-import '~/behaviors/markdown/render_gfm';
import { createDraft } from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
const NoteableNoteStub = stubComponent(NoteableNote, {
template: `
<div>
<slot name="note-header-info">Test</slot>
+ <slot name="after-note-body">Test</slot>
</div>
`,
});
@@ -29,7 +30,6 @@ describe('Batch comments draft note component', () => {
},
};
- const getList = () => getByRole(wrapper.element, 'list');
const findSubmitReviewButton = () => wrapper.findComponent(PublishButton);
const findAddCommentButton = () => wrapper.findComponent(GlButton);
@@ -189,7 +189,7 @@ describe('Batch comments draft note component', () => {
});
it(`calls store ${expectedCalls.length} times on ${event}`, () => {
- getList().dispatchEvent(new MouseEvent(event, { bubbles: true }));
+ wrapper.element.dispatchEvent(new MouseEvent(event, { bubbles: true }));
expect(store.dispatch.mock.calls).toEqual(expectedCalls);
});
});
diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js
index 6a104f0c787..6a99294f855 100644
--- a/spec/frontend/batch_comments/components/preview_item_spec.js
+++ b/spec/frontend/batch_comments/components/preview_item_spec.js
@@ -3,9 +3,10 @@ import PreviewItem from '~/batch_comments/components/preview_item.vue';
import { createStore } from '~/batch_comments/stores';
import diffsModule from '~/diffs/store/modules';
import notesModule from '~/notes/stores/modules';
-import '~/behaviors/markdown/render_gfm';
import { createDraft } from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
describe('Batch comments draft preview item component', () => {
let wrapper;
let draft;
diff --git a/spec/frontend/batch_comments/components/publish_dropdown_spec.js b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
index d1b7160d231..e89934c0192 100644
--- a/spec/frontend/batch_comments/components/publish_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
@@ -4,9 +4,10 @@ import Vue from 'vue';
import Vuex from 'vuex';
import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
import { createStore } from '~/mr_notes/stores';
-import '~/behaviors/markdown/render_gfm';
import { createDraft } from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
Vue.use(Vuex);
describe('Batch comments publish dropdown component', () => {
diff --git a/spec/frontend/behaviors/markdown/render_observability_spec.js b/spec/frontend/behaviors/markdown/render_observability_spec.js
new file mode 100644
index 00000000000..c87d11742dc
--- /dev/null
+++ b/spec/frontend/behaviors/markdown/render_observability_spec.js
@@ -0,0 +1,38 @@
+import renderObservability from '~/behaviors/markdown/render_observability';
+import * as ColorUtils from '~/lib/utils/color_utils';
+
+describe('Observability iframe renderer', () => {
+ const findObservabilityIframes = (theme = 'light') =>
+ document.querySelectorAll(`iframe[src="https://observe.gitlab.com/?theme=${theme}&kiosk"]`);
+
+ const renderEmbeddedObservability = () => {
+ renderObservability([...document.querySelectorAll('.js-render-observability')]);
+ jest.runAllTimers();
+ };
+
+ beforeEach(() => {
+ document.body.dataset.page = '';
+ document.body.innerHTML = '';
+ });
+
+ it('renders an observability iframe', () => {
+ document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/"></div>`;
+
+ expect(findObservabilityIframes()).toHaveLength(0);
+
+ renderEmbeddedObservability();
+
+ expect(findObservabilityIframes()).toHaveLength(1);
+ });
+
+ it('renders iframe with dark param when GL has dark theme', () => {
+ document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/"></div>`;
+ jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => true);
+
+ expect(findObservabilityIframes('dark')).toHaveLength(0);
+
+ renderEmbeddedObservability();
+
+ expect(findObservabilityIframes('dark')).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/blob/openapi/index_spec.js b/spec/frontend/blob/openapi/index_spec.js
index 17e718df495..d9d65258516 100644
--- a/spec/frontend/blob/openapi/index_spec.js
+++ b/spec/frontend/blob/openapi/index_spec.js
@@ -21,7 +21,7 @@ describe('OpenAPI blob viewer', () => {
it('initializes SwaggerUI with the correct configuration', () => {
expect(document.body.innerHTML).toContain(
- '<iframe src="/-/sandbox/swagger" sandbox="allow-scripts allow-popups" frameborder="0" width="100%" height="1000"></iframe>',
+ '<iframe src="/-/sandbox/swagger" sandbox="allow-scripts allow-popups allow-forms" frameborder="0" width="100%" height="1000"></iframe>',
);
});
});
diff --git a/spec/frontend/blob_edit/blob_bundle_spec.js b/spec/frontend/blob_edit/blob_bundle_spec.js
index 644539308c2..ed42322b0e6 100644
--- a/spec/frontend/blob_edit/blob_bundle_spec.js
+++ b/spec/frontend/blob_edit/blob_bundle_spec.js
@@ -5,8 +5,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import blobBundle from '~/blob_edit/blob_bundle';
import SourceEditor from '~/blob_edit/edit_blob';
+import { createAlert } from '~/flash';
jest.mock('~/blob_edit/edit_blob');
+jest.mock('~/flash');
describe('BlobBundle', () => {
it('does not load SourceEditor by default', () => {
@@ -93,4 +95,26 @@ describe('BlobBundle', () => {
});
});
});
+
+ describe('Error handling', () => {
+ let message;
+ beforeEach(() => {
+ setHTMLFixture(`<div class="js-edit-blob-form" data-blob-filename="blah"></div>`);
+ message = 'Foo';
+ SourceEditor.mockImplementation(() => {
+ throw new Error(message);
+ });
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ SourceEditor.mockClear();
+ });
+
+ it('correctly outputs error message when it occurs', async () => {
+ blobBundle();
+ await waitForPromises();
+ expect(createAlert).toHaveBeenCalledWith({ message });
+ });
+ });
});
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 3a2beb714e9..34c0504143c 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -198,6 +198,13 @@ describe('Board list component', () => {
expect(findDraggable().exists()).toBe(true);
});
+ it('sets delay and delayOnTouchOnly attributes on board list', () => {
+ const listEl = wrapper.findComponent({ ref: 'list' });
+
+ expect(listEl.attributes('delay')).toBe('100');
+ expect(listEl.attributes('delayontouchonly')).toBe('true');
+ });
+
describe('handleDragOnStart', () => {
it('adds a class `is-dragging` to document body', () => {
expect(document.body.classList.contains('is-dragging')).toBe(false);
@@ -269,6 +276,10 @@ describe('Board list component', () => {
it('Draggable is not used', () => {
expect(findDraggable().exists()).toBe(false);
});
+
+ it('Board card move to position is not visible', () => {
+ expect(findMoveToPositionComponent().exists()).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 7e35c39cd48..0d5b1d16e30 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -12,7 +12,7 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue
import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
-import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import SidebarLabelsWidget from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
import { mockActiveIssue, mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
Vue.use(Vuex);
@@ -146,6 +146,20 @@ describe('BoardContentSidebar', () => {
expect(wrapper.findComponent(SidebarSeverity).exists()).toBe(false);
});
+ it('does not render SidebarHealthStatusWidget', async () => {
+ const SidebarHealthStatusWidget = (
+ await import('ee_component/sidebar/components/health_status/sidebar_health_status_widget.vue')
+ ).default;
+ expect(wrapper.findComponent(SidebarHealthStatusWidget).exists()).toBe(false);
+ });
+
+ it('does not render SidebarWeightWidget', async () => {
+ const SidebarWeightWidget = (
+ await import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue')
+ ).default;
+ expect(wrapper.findComponent(SidebarWeightWidget).exists()).toBe(false);
+ });
+
describe('when we emit close', () => {
let toggleBoardItem;
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index b2138700602..82e7ab48e7d 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -123,15 +123,39 @@ describe('BoardContent', () => {
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
- it('resizes the list on resize', async () => {
+ it('on small screens, sets board container height to full height', async () => {
window.innerHeight = 1000;
+ window.innerWidth = 767;
jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ top: 100 });
wrapper.vm.resizeObserver.trigger();
await nextTick();
- expect(wrapper.findComponent({ ref: 'list' }).attributes('style')).toBe('height: 900px;');
+ const style = wrapper.findComponent({ ref: 'list' }).attributes('style');
+
+ expect(style).toBe('height: 1000px;');
+ });
+
+ it('on large screens, sets board container height fill area below filters', async () => {
+ window.innerHeight = 1000;
+ window.innerWidth = 768;
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ top: 100 });
+
+ wrapper.vm.resizeObserver.trigger();
+
+ await nextTick();
+
+ const style = wrapper.findComponent({ ref: 'list' }).attributes('style');
+
+ expect(style).toBe('height: 900px;');
+ });
+
+ it('sets delay and delayOnTouchOnly attributes on board list', () => {
+ const listEl = wrapper.findComponent({ ref: 'list' });
+
+ expect(listEl.attributes('delay')).toBe('100');
+ expect(listEl.attributes('delayontouchonly')).toBe('true');
});
});
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index 6f17e4193a3..e80c66f7fb8 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -17,7 +17,7 @@ import {
TOKEN_TYPE_WEIGHT,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { createStore } from '~/boards/stores';
@@ -30,7 +30,7 @@ describe('BoardFilteredSearch', () => {
{
icon: 'labels',
title: TOKEN_TITLE_LABEL,
- type: 'label',
+ type: TOKEN_TYPE_LABEL,
operators: [
{ value: '=', description: 'is' },
{ value: '!=', description: 'is not' },
@@ -43,15 +43,15 @@ describe('BoardFilteredSearch', () => {
{
icon: 'pencil',
title: TOKEN_TITLE_AUTHOR,
- type: 'author',
+ type: TOKEN_TYPE_AUTHOR,
operators: [
{ value: '=', description: 'is' },
{ value: '!=', description: 'is not' },
],
symbol: '@',
- token: AuthorToken,
+ token: UserToken,
unique: true,
- fetchAuthors: () => new Promise(() => {}),
+ fetchUsers: () => new Promise(() => {}),
},
];
@@ -109,7 +109,7 @@ describe('BoardFilteredSearch', () => {
createComponent({ props: { eeFilters: { labelName: ['label'] } } });
expect(findFilteredSearch().props('initialFilterValue')).toEqual([
- { type: 'label', value: { data: 'label', operator: '=' } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'label', operator: '=' } },
]);
});
});
@@ -158,7 +158,9 @@ describe('BoardFilteredSearch', () => {
['None', url('None')],
['Any', url('Any')],
])('sets the url param %s', (assigneeParam, expected) => {
- const mockFilters = [{ type: 'assignee', value: { data: assigneeParam, operator: '=' } }];
+ const mockFilters = [
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: assigneeParam, operator: '=' } },
+ ];
jest.spyOn(urlUtility, 'updateHistory');
findFilteredSearch().vm.$emit('onFilter', mockFilters);
diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
index e4a6a2b8b76..513561307cd 100644
--- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
@@ -23,14 +23,14 @@ describe('IssueBoardFilter', () => {
});
};
- let fetchAuthorsSpy;
+ let fetchUsersSpy;
let fetchLabelsSpy;
beforeEach(() => {
- fetchAuthorsSpy = jest.fn();
+ fetchUsersSpy = jest.fn();
fetchLabelsSpy = jest.fn();
issueBoardFilters.mockReturnValue({
- fetchAuthors: fetchAuthorsSpy,
+ fetchUsers: fetchUsersSpy,
fetchLabels: fetchLabelsSpy,
});
});
@@ -59,7 +59,7 @@ describe('IssueBoardFilter', () => {
const tokens = mockTokens(
fetchLabelsSpy,
- fetchAuthorsSpy,
+ fetchUsersSpy,
wrapper.vm.fetchMilestones,
isSignedIn,
);
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
index 5c435643425..e2e4baefad0 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
@@ -42,13 +42,20 @@ describe('BoardSidebarTimeTracker', () => {
wrapper = null;
});
- it.each([[true], [false]])(
- 'renders IssuableTimeTracker with correct spent and estimated time (timeTrackingLimitToHours=%s)',
- (timeTrackingLimitToHours) => {
- createComponent({ provide: { timeTrackingLimitToHours } });
+ it.each`
+ timeTrackingLimitToHours | canUpdate
+ ${true} | ${false}
+ ${true} | ${true}
+ ${false} | ${false}
+ ${false} | ${true}
+ `(
+ 'renders IssuableTimeTracker with correct spent and estimated time (timeTrackingLimitToHours=$timeTrackingLimitToHours, canUpdate=$canUpdate)',
+ ({ timeTrackingLimitToHours, canUpdate }) => {
+ createComponent({ provide: { timeTrackingLimitToHours, canUpdate } });
expect(wrapper.findComponent(IssuableTimeTracker).props()).toEqual({
limitToHours: timeTrackingLimitToHours,
+ canAddTimeEntries: canUpdate,
showCollapsed: false,
issuableId: '1',
issuableIid: '1',
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 3c26fa97338..df41eb05eae 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -2,22 +2,26 @@ import { GlFilteredSearchToken } from '@gitlab/ui';
import { keyBy } from 'lodash';
import { ListType } from '~/boards/constants';
import {
- OPERATOR_IS_AND_IS_NOT,
- OPERATOR_IS_ONLY,
+ OPERATORS_IS,
+ OPERATORS_IS_NOT,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_CONFIDENTIAL,
TOKEN_TITLE_LABEL,
TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_MY_REACTION,
TOKEN_TITLE_RELEASE,
TOKEN_TITLE_TYPE,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
@@ -733,54 +737,54 @@ export const mockMoveData = {
};
export const mockEmojiToken = {
- type: 'my-reaction',
+ type: TOKEN_TYPE_MY_REACTION,
icon: 'thumb-up',
- title: 'My-Reaction',
+ title: TOKEN_TITLE_MY_REACTION,
unique: true,
token: EmojiToken,
fetchEmojis: expect.any(Function),
};
export const mockConfidentialToken = {
- type: 'confidential',
+ type: TOKEN_TYPE_CONFIDENTIAL,
icon: 'eye-slash',
- title: 'Confidential',
+ title: TOKEN_TITLE_CONFIDENTIAL,
unique: true,
token: GlFilteredSearchToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
options: [
{ icon: 'eye-slash', value: 'yes', title: 'Yes' },
{ icon: 'eye', value: 'no', title: 'No' },
],
};
-export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedIn) => [
+export const mockTokens = (fetchLabels, fetchUsers, fetchMilestones, isSignedIn) => [
{
icon: 'user',
title: TOKEN_TITLE_ASSIGNEE,
type: TOKEN_TYPE_ASSIGNEE,
- operators: OPERATOR_IS_AND_IS_NOT,
- token: AuthorToken,
+ operators: OPERATORS_IS_NOT,
+ token: UserToken,
unique: true,
- fetchAuthors,
- preloadedAuthors: [],
+ fetchUsers,
+ preloadedUsers: [],
},
{
icon: 'pencil',
title: TOKEN_TITLE_AUTHOR,
type: TOKEN_TYPE_AUTHOR,
- operators: OPERATOR_IS_AND_IS_NOT,
+ operators: OPERATORS_IS_NOT,
symbol: '@',
- token: AuthorToken,
+ token: UserToken,
unique: true,
- fetchAuthors,
- preloadedAuthors: [],
+ fetchUsers,
+ preloadedUsers: [],
},
{
icon: 'labels',
title: TOKEN_TITLE_LABEL,
type: TOKEN_TYPE_LABEL,
- operators: OPERATOR_IS_AND_IS_NOT,
+ operators: OPERATORS_IS_NOT,
token: LabelToken,
unique: false,
symbol: '~',
diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js
index 7ff34ffdf9e..4324e7068e0 100644
--- a/spec/frontend/boards/project_select_spec.js
+++ b/spec/frontend/boards/project_select_spec.js
@@ -156,7 +156,7 @@ describe('ProjectSelect component', () => {
});
it('renders the name of the selected project', () => {
- expect(findGlDropdown().find('.gl-new-dropdown-button-text').text()).toBe(
+ expect(findGlDropdown().find('.gl-dropdown-button-text').text()).toBe(
mockProjectsList1[0].name,
);
});
diff --git a/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js b/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
index 553ca52f9ce..b2a25bc93ea 100644
--- a/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
+++ b/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
@@ -4,7 +4,10 @@ import { registerCaptchaModalInterceptor } from '~/captcha/captcha_modal_axios_i
import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error';
import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import httpStatusCodes, {
+ HTTP_STATUS_CONFLICT,
+ HTTP_STATUS_METHOD_NOT_ALLOWED,
+} from '~/lib/utils/http_status';
jest.mock('~/captcha/wait_for_captcha_to_be_solved');
@@ -33,7 +36,7 @@ describe('registerCaptchaModalInterceptor', () => {
mock.onAny('/endpoint-with-unrelated-error').reply(404, AXIOS_RESPONSE);
mock.onAny('/endpoint-with-captcha').reply((config) => {
if (!supportedMethods.includes(config.method)) {
- return [httpStatusCodes.METHOD_NOT_ALLOWED, { method: config.method }];
+ return [HTTP_STATUS_METHOD_NOT_ALLOWED, { method: config.method }];
}
const data = JSON.parse(config.data);
@@ -46,7 +49,7 @@ describe('registerCaptchaModalInterceptor', () => {
return [httpStatusCodes.OK, { ...data, method: config.method, CAPTCHA_SUCCESS }];
}
- return [httpStatusCodes.CONFLICT, NEEDS_CAPTCHA_RESPONSE];
+ return [HTTP_STATUS_CONFLICT, NEEDS_CAPTCHA_RESPONSE];
});
axios.interceptors.response.handlers = [];
@@ -123,7 +126,7 @@ describe('registerCaptchaModalInterceptor', () => {
await expect(() => axios[method]('/endpoint-with-captcha')).rejects.toThrow(
expect.objectContaining({
response: expect.objectContaining({
- status: httpStatusCodes.METHOD_NOT_ALLOWED,
+ status: HTTP_STATUS_METHOD_NOT_ALLOWED,
data: { method },
}),
}),
diff --git a/spec/frontend/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
index ea69a80274e..d4f588a0e09 100644
--- a/spec/frontend/ci_lint/components/ci_lint_spec.js
+++ b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
@@ -2,9 +2,9 @@ import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import CiLint from '~/ci_lint/components/ci_lint.vue';
-import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
-import lintCIMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
+import CiLint from '~/ci/ci_lint/components/ci_lint.vue';
+import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue';
+import lintCIMutation from '~/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
import { mockLintDataValid } from '../mock_data';
diff --git a/spec/frontend/ci_lint/mock_data.js b/spec/frontend/ci/ci_lint/mock_data.js
index 660b2ad6e8b..05582470dfa 100644
--- a/spec/frontend/ci_lint/mock_data.js
+++ b/spec/frontend/ci/ci_lint/mock_data.js
@@ -1,4 +1,4 @@
-import { mockJobs } from 'jest/pipeline_editor/mock_data';
+import { mockJobs } from 'jest/ci/pipeline_editor/mock_data';
export const mockLintDataError = {
data: {
diff --git a/spec/frontend/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js b/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
index d03f12bc249..b00e1adab63 100644
--- a/spec/frontend/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
@@ -3,8 +3,8 @@ import { mount } from '@vue/test-utils';
import { merge } from 'lodash';
import { TEST_HOST } from 'helpers/test_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import CodeSnippetAlert from '~/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue';
-import { CODE_SNIPPET_SOURCE_API_FUZZING } from '~/pipeline_editor/components/code_snippet_alert/constants';
+import CodeSnippetAlert from '~/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue';
+import { CODE_SNIPPET_SOURCE_API_FUZZING } from '~/ci/pipeline_editor/components/code_snippet_alert/constants';
const apiFuzzingConfigurationPath = '/namespace/project/-/security/configuration/api_fuzzing';
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
index 0ee6da9d329..8e1d8081dd8 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
@@ -2,7 +2,7 @@ import { nextTick } from 'vue';
import { GlFormInput, GlFormTextarea } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
+import CommitForm from '~/ci/pipeline_editor/components/commit/commit_form.vue';
import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
index 744b0378a75..f6e93c55bbb 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
@@ -4,18 +4,18 @@ import { mount } from '@vue/test-utils';
import Vue from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
-import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
+import CommitForm from '~/ci/pipeline_editor/components/commit/commit_form.vue';
+import CommitSection from '~/ci/pipeline_editor/components/commit/commit_section.vue';
import {
COMMIT_ACTION_CREATE,
COMMIT_ACTION_UPDATE,
COMMIT_SUCCESS,
COMMIT_SUCCESS_WITH_REDIRECT,
-} from '~/pipeline_editor/constants';
-import { resolvers } from '~/pipeline_editor/graphql/resolvers';
-import commitCreate from '~/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql';
-import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
-import updatePipelineEtag from '~/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql';
+} from '~/ci/pipeline_editor/constants';
+import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
+import commitCreate from '~/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql';
+import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
+import updatePipelineEtag from '~/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql';
import {
mockCiConfigPath,
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
index 7e1e5004d91..137137ec657 100644
--- a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
@@ -1,8 +1,8 @@
import { getByRole } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
-import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants';
+import FirstPipelineCard from '~/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
+import { pipelineEditorTrackingOptions } from '~/ci/pipeline_editor/constants';
describe('First pipeline card', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
index c592e959068..cdce757ce7c 100644
--- a/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
+import GettingStartedCard from '~/ci/pipeline_editor/components/drawer/cards/getting_started_card.vue';
describe('Getting started card', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
index 49177befe0e..6909916c3e6 100644
--- a/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
@@ -1,8 +1,8 @@
import { getByRole } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue';
-import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants';
+import PipelineConfigReferenceCard from '~/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue';
+import { pipelineEditorTrackingOptions } from '~/ci/pipeline_editor/constants';
describe('Pipeline config reference card', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
index bebd2484c1d..0c6879020de 100644
--- a/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import VisualizeAndLintCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
+import VisualizeAndLintCard from '~/ci/pipeline_editor/components/drawer/cards/getting_started_card.vue';
describe('Visual and Lint card', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
index 33b53bf6a56..42e372cc1db 100644
--- a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlDrawer } from '@gitlab/ui';
-import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
+import PipelineEditorDrawer from '~/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
describe('Pipeline editor drawer', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
index edd2b45569a..f510c61ee74 100644
--- a/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import DemoJobPill from '~/pipeline_editor/components/drawer/ui/demo_job_pill.vue';
+import DemoJobPill from '~/ci/pipeline_editor/components/drawer/ui/demo_job_pill.vue';
describe('Demo job pill', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
index 7dd8a77d055..2a2bc2547cc 100644
--- a/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
@@ -2,7 +2,7 @@ import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { EDITOR_READY_EVENT } from '~/editor/constants';
-import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
+import CiConfigMergedPreview from '~/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import { mockLintResponse, mockCiConfigPath } from '../../mock_data';
describe('Text editor component', () => {
diff --git a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
index 930f08ef545..d7f0ce838d6 100644
--- a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue';
+import CiEditorHeader from '~/ci/pipeline_editor/components/editor/ci_editor_header.vue';
import {
pipelineEditorTrackingOptions,
TEMPLATE_REPOSITORY_URL,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
describe('CI Editor Header', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
index 6cdf9a93d55..63e23c41263 100644
--- a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { EDITOR_READY_EVENT } from '~/editor/constants';
-import { SOURCE_EDITOR_DEBOUNCE } from '~/pipeline_editor/constants';
-import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
+import { SOURCE_EDITOR_DEBOUNCE } from '~/ci/pipeline_editor/constants';
+import TextEditor from '~/ci/pipeline_editor/components/editor/text_editor.vue';
import {
mockCiConfigPath,
mockCiYml,
diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
index f0347ad19ac..a26232df58f 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -9,12 +9,12 @@ import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
-import { DEFAULT_FAILURE } from '~/pipeline_editor/constants';
-import getAvailableBranchesQuery from '~/pipeline_editor/graphql/queries/available_branches.query.graphql';
-import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
-import getLastCommitBranch from '~/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql';
-import { resolvers } from '~/pipeline_editor/graphql/resolvers';
+import BranchSwitcher from '~/ci/pipeline_editor/components/file_nav/branch_switcher.vue';
+import { DEFAULT_FAILURE } from '~/ci/pipeline_editor/constants';
+import getAvailableBranchesQuery from '~/ci/pipeline_editor/graphql/queries/available_branches.query.graphql';
+import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
+import getLastCommitBranch from '~/ci/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql';
+import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import {
mockBranchPaginationLimit,
diff --git a/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
index d503aff40b8..907db16913c 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
@@ -3,15 +3,15 @@ import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
-import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
-import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
-import FileTreePopover from '~/pipeline_editor/components/popovers/file_tree_popover.vue';
-import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+import BranchSwitcher from '~/ci/pipeline_editor/components/file_nav/branch_switcher.vue';
+import PipelineEditorFileNav from '~/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
+import FileTreePopover from '~/ci/pipeline_editor/components/popovers/file_tree_popover.vue';
+import getAppStatus from '~/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import {
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipeline_editor/components/file-tree/container_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
index f79074f1e0f..11ba517e0eb 100644
--- a/spec/frontend/pipeline_editor/components/file-tree/container_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
@@ -3,9 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { createMockDirective } from 'helpers/vue_mock_directive';
-import PipelineEditorFileTreeContainer from '~/pipeline_editor/components/file_tree/container.vue';
-import PipelineEditorFileTreeItem from '~/pipeline_editor/components/file_tree/file_item.vue';
-import { FILE_TREE_TIP_DISMISSED_KEY } from '~/pipeline_editor/constants';
+import PipelineEditorFileTreeContainer from '~/ci/pipeline_editor/components/file_tree/container.vue';
+import PipelineEditorFileTreeItem from '~/ci/pipeline_editor/components/file_tree/file_item.vue';
+import { FILE_TREE_TIP_DISMISSED_KEY } from '~/ci/pipeline_editor/constants';
import { mockCiConfigPath, mockIncludes, mockIncludesHelpPagePath } from '../../mock_data';
describe('Pipeline editor file nav', () => {
diff --git a/spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
index f12ac14c6be..bceb741f91c 100644
--- a/spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
@@ -1,7 +1,7 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import FileIcon from '~/vue_shared/components/file_icon.vue';
-import PipelineEditorFileTreeItem from '~/pipeline_editor/components/file_tree/file_item.vue';
+import PipelineEditorFileTreeItem from '~/ci/pipeline_editor/components/file_tree/file_item.vue';
import { mockIncludesWithBlob, mockDefaultIncludes } from '../../mock_data';
describe('Pipeline editor file nav', () => {
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
index e1dc08b637f..555b9f29fbf 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
-import PipelineStatus from '~/pipeline_editor/components/header/pipeline_status.vue';
-import ValidationSegment from '~/pipeline_editor/components/header/validation_segment.vue';
+import PipelineEditorHeader from '~/ci/pipeline_editor/components/header/pipeline_editor_header.vue';
+import PipelineStatus from '~/ci/pipeline_editor/components/header/pipeline_status.vue';
+import ValidationSegment from '~/ci/pipeline_editor/components/header/validation_segment.vue';
import { mockCiYml, mockLintResponse } from '../../mock_data';
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
index d40a9cc8100..6f28362e478 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
@@ -3,10 +3,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
+import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
-import { PIPELINE_FAILURE } from '~/pipeline_editor/constants';
+import { PIPELINE_FAILURE } from '~/ci/pipeline_editor/constants';
import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
index 35315db39f8..a62c51ffb59 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
@@ -4,9 +4,9 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import PipelineStatus, { i18n } from '~/pipeline_editor/components/header/pipeline_status.vue';
-import getPipelineQuery from '~/pipeline_editor/graphql/queries/pipeline.query.graphql';
-import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
+import PipelineStatus, { i18n } from '~/ci/pipeline_editor/components/header/pipeline_status.vue';
+import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
+import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import { mockCommitSha, mockProjectPipeline, mockProjectFullPath } from '../../mock_data';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
index d40a9cc8100..6f28362e478 100644
--- a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
@@ -3,10 +3,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
+import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
-import { PIPELINE_FAILURE } from '~/pipeline_editor/constants';
+import { PIPELINE_FAILURE } from '~/ci/pipeline_editor/constants';
import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
index 1ad621e6f45..0853a6f4ca4 100644
--- a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
@@ -8,8 +8,8 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { sprintf } from '~/locale';
import ValidationSegment, {
i18n,
-} from '~/pipeline_editor/components/header/validation_segment.vue';
-import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+} from '~/ci/pipeline_editor/components/header/validation_segment.vue';
+import getAppStatus from '~/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import {
CI_CONFIG_STATUS_INVALID,
EDITOR_APP_STATUS_EMPTY,
@@ -17,7 +17,7 @@ import {
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_LINT_UNAVAILABLE,
EDITOR_APP_STATUS_VALID,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
import {
mergeUnwrappedCiConfig,
mockCiYml,
diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
index 7f89eda4dff..d43bdec3a33 100644
--- a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
@@ -1,7 +1,7 @@
import { GlTableLite, GlLink } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
+import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue';
import { mockJobs, mockErrors, mockWarnings } from '../../mock_data';
describe('CI Lint Results', () => {
diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
index 36052a2e16a..b5e3ea06c2c 100644
--- a/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
-import CiLintWarnings from '~/pipeline_editor/components/lint/ci_lint_warnings.vue';
+import CiLintWarnings from '~/ci/pipeline_editor/components/lint/ci_lint_warnings.vue';
const warnings = ['warning 1', 'warning 2', 'warning 3'];
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 27707f8b01a..70310cbdb10 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -6,11 +6,11 @@ import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
-import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
-import CiValidate from '~/pipeline_editor/components/validate/ci_validate.vue';
-import WalkthroughPopover from '~/pipeline_editor/components/popovers/walkthrough_popover.vue';
-import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
-import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
+import CiConfigMergedPreview from '~/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue';
+import CiValidate from '~/ci/pipeline_editor/components/validate/ci_validate.vue';
+import WalkthroughPopover from '~/ci/pipeline_editor/components/popovers/walkthrough_popover.vue';
+import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
+import EditorTab from '~/ci/pipeline_editor/components/ui/editor_tab.vue';
import {
CREATE_TAB,
EDITOR_APP_STATUS_EMPTY,
@@ -20,9 +20,9 @@ import {
TAB_QUERY_PARAM,
VALIDATE_TAB,
VALIDATE_TAB_BADGE_DISMISSED_KEY,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
-import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
+import getBlobContent from '~/ci/pipeline_editor/graphql/queries/blob_content.query.graphql';
import {
mockBlobContentQueryResponse,
mockCiLintPath,
diff --git a/spec/frontend/pipeline_editor/components/popovers/file_tree_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
index 98ce3f6ea40..63ebfc0559d 100644
--- a/spec/frontend/pipeline_editor/components/popovers/file_tree_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
@@ -1,8 +1,8 @@
import { nextTick } from 'vue';
import { GlLink, GlPopover, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import FileTreePopover from '~/pipeline_editor/components/popovers/file_tree_popover.vue';
-import { FILE_TREE_POPOVER_DISMISSED_KEY } from '~/pipeline_editor/constants';
+import FileTreePopover from '~/ci/pipeline_editor/components/popovers/file_tree_popover.vue';
+import { FILE_TREE_POPOVER_DISMISSED_KEY } from '~/ci/pipeline_editor/constants';
import { mockIncludesHelpPagePath } from '../../mock_data';
describe('FileTreePopover component', () => {
diff --git a/spec/frontend/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
index 97f785a71bc..cf0b974081e 100644
--- a/spec/frontend/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
@@ -1,7 +1,7 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ValidatePopover from '~/pipeline_editor/components/popovers/validate_pipeline_popover.vue';
-import { VALIDATE_TAB_FEEDBACK_URL } from '~/pipeline_editor/constants';
+import ValidatePopover from '~/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue';
+import { VALIDATE_TAB_FEEDBACK_URL } from '~/ci/pipeline_editor/constants';
import { mockSimulatePipelineHelpPagePath } from '../../mock_data';
describe('ValidatePopover component', () => {
diff --git a/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
index b86c82850c5..ca6033f2ff5 100644
--- a/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
@@ -1,6 +1,6 @@
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
-import WalkthroughPopover from '~/pipeline_editor/components/popovers/walkthrough_popover.vue';
+import WalkthroughPopover from '~/ci/pipeline_editor/components/popovers/walkthrough_popover.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
Vue.config.ignoredElements = ['gl-emoji'];
diff --git a/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
index 44fda2812d8..b22c98e5544 100644
--- a/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import ConfirmDialog from '~/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue';
+import ConfirmDialog from '~/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue';
describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => {
let beforeUnloadEvent;
diff --git a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
index 24f27e8c5fb..a4e7abba7b0 100644
--- a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlBadge, GlTabs } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
+import EditorTab from '~/ci/pipeline_editor/components/ui/editor_tab.vue';
const mockContent1 = 'MOCK CONTENT 1';
const mockContent2 = 'MOCK CONTENT 2';
@@ -10,7 +10,7 @@ const MockSourceEditor = {
template: '<div>EDITOR</div>',
};
-describe('~/pipeline_editor/components/ui/editor_tab.vue', () => {
+describe('~/ci/pipeline_editor/components/ui/editor_tab.vue', () => {
let wrapper;
let mockChildMounted = jest.fn();
diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
index c76c3460e99..3c68f74af43 100644
--- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
@@ -1,7 +1,7 @@
import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
-import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
+import PipelineEditorFileNav from '~/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
+import PipelineEditorEmptyState from '~/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
describe('Pipeline editor empty state', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
index d9ecee31e83..fdb3be5c690 100644
--- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
@@ -2,9 +2,9 @@ import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
-import CodeSnippetAlert from '~/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue';
-import { CODE_SNIPPET_SOURCES } from '~/pipeline_editor/components/code_snippet_alert/constants';
-import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue';
+import CodeSnippetAlert from '~/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue';
+import { CODE_SNIPPET_SOURCES } from '~/ci/pipeline_editor/components/code_snippet_alert/constants';
+import PipelineEditorMessages from '~/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue';
import {
COMMIT_FAILURE,
COMMIT_SUCCESS,
@@ -13,7 +13,7 @@ import {
DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN,
PIPELINE_FAILURE,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
beforeEach(() => {
setWindowLocation(TEST_HOST);
diff --git a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
index 09d4f9736ad..ae25142b455 100644
--- a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
@@ -5,12 +5,12 @@ import VueApollo from 'vue-apollo';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
-import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
-import CiValidate, { i18n } from '~/pipeline_editor/components/validate/ci_validate.vue';
-import ValidatePipelinePopover from '~/pipeline_editor/components/popovers/validate_pipeline_popover.vue';
-import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
-import lintCIMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
-import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants';
+import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue';
+import CiValidate, { i18n } from '~/ci/pipeline_editor/components/validate/ci_validate.vue';
+import ValidatePipelinePopover from '~/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue';
+import getBlobContent from '~/ci/pipeline_editor/graphql/queries/blob_content.query.graphql';
+import lintCIMutation from '~/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
+import { pipelineEditorTrackingOptions } from '~/ci/pipeline_editor/constants';
import {
mockBlobContentQueryResponse,
mockCiLintPath,
diff --git a/spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap b/spec/frontend/ci/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap
index ee5a3cb288f..75a1354fd29 100644
--- a/spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap
+++ b/spec/frontend/ci/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`~/pipeline_editor/graphql/resolvers Mutation lintCI lint data is as expected 1`] = `
+exports[`~/ci/pipeline_editor/graphql/resolvers Mutation lintCI lint data is as expected 1`] = `
Object {
"__typename": "CiLintContent",
"errors": Array [],
diff --git a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
index 76ae96c623a..e54c72a758f 100644
--- a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js
+++ b/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
-import { resolvers } from '~/pipeline_editor/graphql/resolvers';
+import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import { mockLintResponse } from '../mock_data';
jest.mock('~/api', () => {
@@ -10,7 +10,7 @@ jest.mock('~/api', () => {
};
});
-describe('~/pipeline_editor/graphql/resolvers', () => {
+describe('~/ci/pipeline_editor/graphql/resolvers', () => {
describe('Mutation', () => {
describe('lintCI', () => {
let mock;
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js
index 2ea580b7b53..176dc24f169 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/mock_data.js
@@ -1,4 +1,4 @@
-import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
+import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/ci/pipeline_editor/constants';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
export const mockProjectNamespace = 'user1';
@@ -119,7 +119,7 @@ export const mockIncludes = [
];
// Mock result of the graphql query at:
-// app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
+// app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.graphql
export const mockCiConfigQueryResponse = {
data: {
ciConfig: {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
index 9fe1536d3f5..2246d0bbf7e 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -6,30 +6,30 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
-import { resolvers } from '~/pipeline_editor/graphql/resolvers';
-import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
-import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
-import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue';
-import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
+import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
+import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
+import PipelineEditorEmptyState from '~/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
+import PipelineEditorMessages from '~/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue';
+import PipelineEditorHeader from '~/ci/pipeline_editor/components/header/pipeline_editor_header.vue';
import ValidationSegment, {
i18n as validationSegmenti18n,
-} from '~/pipeline_editor/components/header/validation_segment.vue';
+} from '~/ci/pipeline_editor/components/header/validation_segment.vue';
import {
COMMIT_SUCCESS,
COMMIT_SUCCESS_WITH_REDIRECT,
COMMIT_FAILURE,
EDITOR_APP_STATUS_LOADING,
-} from '~/pipeline_editor/constants';
-import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
-import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
-import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
-import getLatestCommitShaQuery from '~/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql';
-import getPipelineQuery from '~/pipeline_editor/graphql/queries/pipeline.query.graphql';
-import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
-import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
-
-import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
-import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
+} from '~/ci/pipeline_editor/constants';
+import getBlobContent from '~/ci/pipeline_editor/graphql/queries/blob_content.query.graphql';
+import getCiConfigData from '~/ci/pipeline_editor/graphql/queries/ci_config.query.graphql';
+import getTemplate from '~/ci/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
+import getLatestCommitShaQuery from '~/ci/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql';
+import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
+import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
+import getAppStatus from '~/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+
+import PipelineEditorApp from '~/ci/pipeline_editor/pipeline_editor_app.vue';
+import PipelineEditorHome from '~/ci/pipeline_editor/pipeline_editor_home.vue';
import {
mockCiConfigPath,
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
index 2b06660c4b3..621e015e825 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
@@ -3,14 +3,14 @@ import { nextTick } from 'vue';
import { GlButton, GlDrawer, GlModal } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
-import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue';
-import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
-import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
-import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
-import PipelineEditorFileTree from '~/pipeline_editor/components/file_tree/container.vue';
-import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
-import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
-import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
+import CiEditorHeader from '~/ci/pipeline_editor/components/editor/ci_editor_header.vue';
+import CommitSection from '~/ci/pipeline_editor/components/commit/commit_section.vue';
+import PipelineEditorDrawer from '~/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
+import PipelineEditorFileNav from '~/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
+import PipelineEditorFileTree from '~/ci/pipeline_editor/components/file_tree/container.vue';
+import BranchSwitcher from '~/ci/pipeline_editor/components/file_nav/branch_switcher.vue';
+import PipelineEditorHeader from '~/ci/pipeline_editor/components/header/pipeline_editor_header.vue';
+import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
import {
CREATE_TAB,
FILE_TREE_DISPLAY_KEY,
@@ -18,8 +18,8 @@ import {
MERGED_TAB,
TABS_INDEX,
VISUALIZE_TAB,
-} from '~/pipeline_editor/constants';
-import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
+} from '~/ci/pipeline_editor/constants';
+import PipelineEditorHome from '~/ci/pipeline_editor/pipeline_editor_home.vue';
import { mockLintResponse, mockCiYml } from './mock_data';
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
index e5d9b378a42..639c2dbef4c 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
@@ -1,25 +1,160 @@
-import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
import { GlForm } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
import PipelineSchedulesForm from '~/ci/pipeline_schedules/components/pipeline_schedules_form.vue';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
+import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
+import { timezoneDataFixture } from '../../../vue_shared/components/timezone_dropdown/helpers';
describe('Pipeline schedules form', () => {
let wrapper;
+ const defaultBranch = 'main';
+ const projectId = '1';
+ const cron = '';
+ const dailyLimit = '';
- const createComponent = () => {
- wrapper = shallowMount(PipelineSchedulesForm);
+ const createComponent = (mountFn = shallowMountExtended, stubs = {}) => {
+ wrapper = mountFn(PipelineSchedulesForm, {
+ propsData: {
+ timezoneData: timezoneDataFixture,
+ refParam: 'master',
+ },
+ provide: {
+ fullPath: 'gitlab-org/gitlab',
+ projectId,
+ defaultBranch,
+ cron,
+ cronTimezone: '',
+ dailyLimit,
+ settingsLink: '',
+ },
+ stubs,
+ });
};
const findForm = () => wrapper.findComponent(GlForm);
+ const findDescription = () => wrapper.findByTestId('schedule-description');
+ const findIntervalComponent = () => wrapper.findComponent(IntervalPatternInput);
+ const findTimezoneDropdown = () => wrapper.findComponent(TimezoneDropdown);
+ const findRefSelector = () => wrapper.findComponent(RefSelector);
+ const findSubmitButton = () => wrapper.findByTestId('schedule-submit-button');
+ const findCancelButton = () => wrapper.findByTestId('schedule-cancel-button');
+ // Variables
+ const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
+ const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
+ const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value');
+ const findRemoveIcons = () => wrapper.findAllByTestId('remove-ci-variable-row');
beforeEach(() => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
+ describe('Form elements', () => {
+ it('displays form', () => {
+ expect(findForm().exists()).toBe(true);
+ });
+
+ it('displays the description input', () => {
+ expect(findDescription().exists()).toBe(true);
+ });
+
+ it('displays the interval pattern component', () => {
+ const intervalPattern = findIntervalComponent();
+
+ expect(intervalPattern.exists()).toBe(true);
+ expect(intervalPattern.props()).toMatchObject({
+ initialCronInterval: cron,
+ dailyLimit,
+ sendNativeErrors: false,
+ });
+ });
+
+ it('displays the Timezone dropdown', () => {
+ const timezoneDropdown = findTimezoneDropdown();
+
+ expect(timezoneDropdown.exists()).toBe(true);
+ expect(timezoneDropdown.props()).toMatchObject({
+ value: '',
+ name: 'schedule-timezone',
+ timezoneData: timezoneDataFixture,
+ });
+ });
+
+ it('displays the branch/tag selector', () => {
+ const refSelector = findRefSelector();
+
+ expect(refSelector.exists()).toBe(true);
+ expect(refSelector.props()).toMatchObject({
+ enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
+ value: defaultBranch,
+ projectId,
+ translations: { dropdownHeader: 'Select target branch or tag' },
+ useSymbolicRefNames: true,
+ state: true,
+ name: '',
+ });
+ });
+
+ it('displays the submit and cancel buttons', () => {
+ expect(findSubmitButton().exists()).toBe(true);
+ expect(findCancelButton().exists()).toBe(true);
+ });
});
- it('displays form', () => {
- expect(findForm().exists()).toBe(true);
+ describe('CI variables', () => {
+ let mock;
+
+ const addVariableToForm = () => {
+ const input = findKeyInputs().at(0);
+ input.element.value = 'test_var_2';
+ input.trigger('change');
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent(mountExtended);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('creates blank variable on input change event', async () => {
+ expect(findVariableRows()).toHaveLength(1);
+
+ addVariableToForm();
+
+ await nextTick();
+
+ expect(findVariableRows()).toHaveLength(2);
+ expect(findKeyInputs().at(1).element.value).toBe('');
+ expect(findValueInputs().at(1).element.value).toBe('');
+ });
+
+ it('does not display remove icon for last row', async () => {
+ addVariableToForm();
+
+ await nextTick();
+
+ expect(findRemoveIcons()).toHaveLength(1);
+ });
+
+ it('removes ci variable row on remove icon button click', async () => {
+ addVariableToForm();
+
+ await nextTick();
+
+ expect(findVariableRows()).toHaveLength(2);
+
+ findRemoveIcons().at(0).trigger('click');
+
+ await nextTick();
+
+ expect(findVariableRows()).toHaveLength(1);
+ });
});
});
diff --git a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
index c32b52d9e77..5ca4b25da9b 100644
--- a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
@@ -1,8 +1,8 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import component from '~/reports/codequality_report/components/codequality_issue_body.vue';
-import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
+import component from '~/ci/reports/codequality_report/components/codequality_issue_body.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/ci/reports/constants';
describe('code quality issue body issue body', () => {
let wrapper;
diff --git a/spec/frontend/reports/codequality_report/mock_data.js b/spec/frontend/ci/reports/codequality_report/mock_data.js
index 2c994116db6..2c994116db6 100644
--- a/spec/frontend/reports/codequality_report/mock_data.js
+++ b/spec/frontend/ci/reports/codequality_report/mock_data.js
diff --git a/spec/frontend/reports/codequality_report/store/actions_spec.js b/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
index 1878b9f44b2..88628210793 100644
--- a/spec/frontend/reports/codequality_report/store/actions_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
@@ -2,10 +2,10 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
-import createStore from '~/reports/codequality_report/store';
-import * as actions from '~/reports/codequality_report/store/actions';
-import * as types from '~/reports/codequality_report/store/mutation_types';
-import { STATUS_NOT_FOUND } from '~/reports/constants';
+import createStore from '~/ci/reports/codequality_report/store';
+import * as actions from '~/ci/reports/codequality_report/store/actions';
+import * as types from '~/ci/reports/codequality_report/store/mutation_types';
+import { STATUS_NOT_FOUND } from '~/ci/reports/constants';
import { reportIssues, parsedReportIssues } from '../mock_data';
const pollInterval = 123;
diff --git a/spec/frontend/reports/codequality_report/store/getters_spec.js b/spec/frontend/ci/reports/codequality_report/store/getters_spec.js
index 646903390ff..f4505204f67 100644
--- a/spec/frontend/reports/codequality_report/store/getters_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/store/getters_spec.js
@@ -1,6 +1,6 @@
-import createStore from '~/reports/codequality_report/store';
-import * as getters from '~/reports/codequality_report/store/getters';
-import { LOADING, ERROR, SUCCESS, STATUS_NOT_FOUND } from '~/reports/constants';
+import createStore from '~/ci/reports/codequality_report/store';
+import * as getters from '~/ci/reports/codequality_report/store/getters';
+import { LOADING, ERROR, SUCCESS, STATUS_NOT_FOUND } from '~/ci/reports/constants';
describe('Codequality reports store getters', () => {
let localState;
diff --git a/spec/frontend/reports/codequality_report/store/mutations_spec.js b/spec/frontend/ci/reports/codequality_report/store/mutations_spec.js
index 6e14cd7438b..22ff86b1040 100644
--- a/spec/frontend/reports/codequality_report/store/mutations_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/store/mutations_spec.js
@@ -1,6 +1,6 @@
-import createStore from '~/reports/codequality_report/store';
-import mutations from '~/reports/codequality_report/store/mutations';
-import { STATUS_NOT_FOUND } from '~/reports/constants';
+import createStore from '~/ci/reports/codequality_report/store';
+import mutations from '~/ci/reports/codequality_report/store/mutations';
+import { STATUS_NOT_FOUND } from '~/ci/reports/constants';
describe('Codequality Reports mutations', () => {
let localState;
diff --git a/spec/frontend/reports/codequality_report/store/utils/codequality_parser_spec.js b/spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js
index 5b77a2c74be..f7d82d2b662 100644
--- a/spec/frontend/reports/codequality_report/store/utils/codequality_parser_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js
@@ -1,5 +1,5 @@
-import { reportIssues, parsedReportIssues } from 'jest/reports/codequality_report/mock_data';
-import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_parser';
+import { reportIssues, parsedReportIssues } from 'jest/ci/reports/codequality_report/mock_data';
+import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser';
describe('Codequality report store utils', () => {
let result;
diff --git a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
index 311a67a3e31..311a67a3e31 100644
--- a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
+++ b/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
diff --git a/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap b/spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap
index b5a4cb42463..b5a4cb42463 100644
--- a/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap
+++ b/spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap
diff --git a/spec/frontend/reports/components/grouped_issues_list_spec.js b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
index cacbde590d6..3e4adfc7794 100644
--- a/spec/frontend/reports/components/grouped_issues_list_spec.js
+++ b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import GroupedIssuesList from '~/reports/components/grouped_issues_list.vue';
-import ReportItem from '~/reports/components/report_item.vue';
+import GroupedIssuesList from '~/ci/reports/components/grouped_issues_list.vue';
+import ReportItem from '~/ci/reports/components/report_item.vue';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
describe('Grouped Issues List', () => {
diff --git a/spec/frontend/reports/components/issue_status_icon_spec.js b/spec/frontend/ci/reports/components/issue_status_icon_spec.js
index 8706f2f8d83..fb13d4407e2 100644
--- a/spec/frontend/reports/components/issue_status_icon_spec.js
+++ b/spec/frontend/ci/reports/components/issue_status_icon_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import ReportItem from '~/reports/components/issue_status_icon.vue';
-import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
+import ReportItem from '~/ci/reports/components/issue_status_icon.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/ci/reports/constants';
describe('IssueStatusIcon', () => {
let wrapper;
diff --git a/spec/frontend/reports/components/report_item_spec.js b/spec/frontend/ci/reports/components/report_item_spec.js
index 60c7e5f2b44..d835d549531 100644
--- a/spec/frontend/reports/components/report_item_spec.js
+++ b/spec/frontend/ci/reports/components/report_item_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
-import { componentNames } from '~/reports/components/issue_body';
-import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
-import ReportItem from '~/reports/components/report_item.vue';
-import { STATUS_SUCCESS } from '~/reports/constants';
+import { componentNames } from '~/ci/reports/components/issue_body';
+import IssueStatusIcon from '~/ci/reports/components/issue_status_icon.vue';
+import ReportItem from '~/ci/reports/components/report_item.vue';
+import { STATUS_SUCCESS } from '~/ci/reports/constants';
describe('ReportItem', () => {
describe('showReportSectionStatusIcon', () => {
diff --git a/spec/frontend/reports/components/report_link_spec.js b/spec/frontend/ci/reports/components/report_link_spec.js
index 2ed0617a598..ba541ba0303 100644
--- a/spec/frontend/reports/components/report_link_spec.js
+++ b/spec/frontend/ci/reports/components/report_link_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import ReportLink from '~/reports/components/report_link.vue';
+import ReportLink from '~/ci/reports/components/report_link.vue';
-describe('app/assets/javascripts/reports/components/report_link.vue', () => {
+describe('app/assets/javascripts/ci/reports/components/report_link.vue', () => {
let wrapper;
afterEach(() => {
diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/ci/reports/components/report_section_spec.js
index cc35b99a199..f032b210184 100644
--- a/spec/frontend/reports/components/report_section_spec.js
+++ b/spec/frontend/ci/reports/components/report_section_spec.js
@@ -1,8 +1,8 @@
import { GlButton } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import ReportItem from '~/reports/components/report_item.vue';
-import ReportSection from '~/reports/components/report_section.vue';
+import ReportItem from '~/ci/reports/components/report_item.vue';
+import ReportSection from '~/ci/reports/components/report_section.vue';
describe('ReportSection component', () => {
let wrapper;
diff --git a/spec/frontend/reports/components/summary_row_spec.js b/spec/frontend/ci/reports/components/summary_row_spec.js
index 778660d9e44..fb2ae5371d5 100644
--- a/spec/frontend/reports/components/summary_row_spec.js
+++ b/spec/frontend/ci/reports/components/summary_row_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import SummaryRow from '~/reports/components/summary_row.vue';
+import SummaryRow from '~/ci/reports/components/summary_row.vue';
describe('Summary row', () => {
let wrapper;
diff --git a/spec/frontend/reports/mock_data/mock_data.js b/spec/frontend/ci/reports/mock_data/mock_data.js
index 2599b0ac365..2599b0ac365 100644
--- a/spec/frontend/reports/mock_data/mock_data.js
+++ b/spec/frontend/ci/reports/mock_data/mock_data.js
diff --git a/spec/frontend/reports/mock_data/new_and_fixed_failures_report.json b/spec/frontend/ci/reports/mock_data/new_and_fixed_failures_report.json
index 6141e5433a6..9018ad5e4cf 100644
--- a/spec/frontend/reports/mock_data/new_and_fixed_failures_report.json
+++ b/spec/frontend/ci/reports/mock_data/new_and_fixed_failures_report.json
@@ -1,11 +1,21 @@
{
"status": "failed",
- "summary": { "total": 11, "resolved": 2, "errored": 0, "failed": 2 },
+ "summary": {
+ "total": 11,
+ "resolved": 2,
+ "errored": 0,
+ "failed": 2
+ },
"suites": [
{
"name": "rspec:pg",
"status": "failed",
- "summary": { "total": 8, "resolved": 2, "errored": 0, "failed": 1 },
+ "summary": {
+ "total": 8,
+ "resolved": 2,
+ "errored": 0,
+ "failed": 1
+ },
"new_failures": [
{
"status": "failed",
@@ -36,7 +46,12 @@
{
"name": "java ant",
"status": "failed",
- "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 1 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 1
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [
@@ -52,4 +67,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/reports/mock_data/new_errors_report.json b/spec/frontend/ci/reports/mock_data/new_errors_report.json
index 6573d23ee50..d3fb570c327 100644
--- a/spec/frontend/reports/mock_data/new_errors_report.json
+++ b/spec/frontend/ci/reports/mock_data/new_errors_report.json
@@ -1,9 +1,19 @@
{
- "summary": { "total": 11, "resolved": 0, "errored": 2, "failed": 0 },
+ "summary": {
+ "total": 11,
+ "resolved": 0,
+ "errored": 2,
+ "failed": 0
+ },
"suites": [
{
"name": "karma",
- "summary": { "total": 3, "resolved": 0, "errored": 2, "failed": 0 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 2,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -26,7 +36,12 @@
},
{
"name": "rspec:pg",
- "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 8,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -35,4 +50,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/reports/mock_data/new_failures_report.json b/spec/frontend/ci/reports/mock_data/new_failures_report.json
index 438f7c82788..03a875b7636 100644
--- a/spec/frontend/reports/mock_data/new_failures_report.json
+++ b/spec/frontend/ci/reports/mock_data/new_failures_report.json
@@ -1,9 +1,19 @@
{
- "summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 2 },
+ "summary": {
+ "total": 11,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 2
+ },
"suites": [
{
"name": "rspec:pg",
- "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 2 },
+ "summary": {
+ "total": 8,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 2
+ },
"new_failures": [
{
"result": "failure",
@@ -28,7 +38,12 @@
},
{
"name": "java ant",
- "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -37,4 +52,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/reports/mock_data/new_failures_with_null_files_report.json b/spec/frontend/ci/reports/mock_data/new_failures_with_null_files_report.json
index 28ee7d194b9..00a35a3d0a7 100644
--- a/spec/frontend/reports/mock_data/new_failures_with_null_files_report.json
+++ b/spec/frontend/ci/reports/mock_data/new_failures_with_null_files_report.json
@@ -1,9 +1,19 @@
{
- "summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 2 },
+ "summary": {
+ "total": 11,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 2
+ },
"suites": [
{
"name": "rspec:pg",
- "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 2 },
+ "summary": {
+ "total": 8,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 2
+ },
"new_failures": [
{
"result": "failure",
@@ -28,7 +38,12 @@
},
{
"name": "java ant",
- "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -37,4 +52,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/reports/mock_data/no_failures_report.json b/spec/frontend/ci/reports/mock_data/no_failures_report.json
index 7da9e0c6211..a48a206208d 100644
--- a/spec/frontend/reports/mock_data/no_failures_report.json
+++ b/spec/frontend/ci/reports/mock_data/no_failures_report.json
@@ -1,11 +1,21 @@
{
"status": "success",
- "summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 11,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"suites": [
{
"name": "rspec:pg",
"status": "success",
- "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 8,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -16,7 +26,12 @@
{
"name": "java ant",
"status": "success",
- "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -25,4 +40,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/reports/mock_data/recent_failures_report.json b/spec/frontend/ci/reports/mock_data/recent_failures_report.json
index c4a5fb78dcd..f4fc2d2e927 100644
--- a/spec/frontend/reports/mock_data/recent_failures_report.json
+++ b/spec/frontend/ci/reports/mock_data/recent_failures_report.json
@@ -1,9 +1,21 @@
{
- "summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 3, "recentlyFailed": 2 },
+ "summary": {
+ "total": 11,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 3,
+ "recentlyFailed": 2
+ },
"suites": [
{
"name": "rspec:pg",
- "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 2, "recentlyFailed": 1 },
+ "summary": {
+ "total": 8,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 2,
+ "recentlyFailed": 1
+ },
"new_failures": [
{
"result": "failure",
@@ -30,7 +42,13 @@
},
{
"name": "java ant",
- "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 1, "recentlyFailed": 1 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 1,
+ "recentlyFailed": 1
+ },
"new_failures": [
{
"result": "failure",
@@ -49,4 +67,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/reports/mock_data/resolved_failures.json b/spec/frontend/ci/reports/mock_data/resolved_failures.json
index 49de6aa840b..15012fb027d 100644
--- a/spec/frontend/reports/mock_data/resolved_failures.json
+++ b/spec/frontend/ci/reports/mock_data/resolved_failures.json
@@ -1,11 +1,21 @@
{
"status": "success",
- "summary": { "total": 11, "resolved": 4, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 11,
+ "resolved": 4,
+ "errored": 0,
+ "failed": 0
+ },
"suites": [
{
"name": "rspec:pg",
"status": "success",
- "summary": { "total": 8, "resolved": 4, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 8,
+ "resolved": 4,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [
{
@@ -18,7 +28,7 @@
{
"status": "success",
"name": "Test#sum when a is 100 and b is 200 returns summary",
- "execution_time": 7.6e-5,
+ "execution_time": 0.000076,
"system_output": null,
"stack_trace": null
}
@@ -46,7 +56,12 @@
{
"name": "java ant",
"status": "success",
- "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -55,4 +70,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
index 7081bc57467..e233268b756 100644
--- a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -1,6 +1,8 @@
import Vue from 'vue';
import { GlTab, GlTabs } from '@gitlab/ui';
+import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -33,12 +35,15 @@ const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
const mockRunnersPath = '/admin/runners';
Vue.use(VueApollo);
+Vue.use(VueRouter);
describe('AdminRunnerShowApp', () => {
let wrapper;
let mockRunnerQuery;
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findTabAt = (i) => wrapper.findAllComponents(GlTab).at(i);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
@@ -113,6 +118,16 @@ describe('AdminRunnerShowApp', () => {
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
+ it.each(['#/', '#/unknown-tab'])('shows details when location hash is `%s`', async (hash) => {
+ setWindowLocation(hash);
+
+ await createComponent({ mountFn: mountExtended });
+
+ expect(findTabs().props('value')).toBe(0);
+ expect(findRunnerDetails().exists()).toBe(true);
+ expect(findRunnersJobs().exists()).toBe(false);
+ });
+
describe('when runner cannot be updated', () => {
beforeEach(async () => {
mockRunnerQueryResult({
@@ -226,7 +241,7 @@ describe('AdminRunnerShowApp', () => {
});
});
- describe('Jobs tab', () => {
+ describe('When showing jobs', () => {
const stubs = {
GlTab,
GlTabs,
@@ -245,6 +260,17 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnersJobs().exists()).toBe(false);
});
+ it('when URL hash links to jobs tab', async () => {
+ mockRunnerQueryResult();
+ setWindowLocation('#/jobs');
+
+ await createComponent({ mountFn: mountExtended });
+
+ expect(findTabs().props('value')).toBe(1);
+ expect(findRunnerDetails().exists()).toBe(false);
+ expect(findRunnersJobs().exists()).toBe(true);
+ });
+
it('without a job count, shows no jobs count', async () => {
mockRunnerQueryResult({ jobCount: null });
@@ -260,7 +286,28 @@ describe('AdminRunnerShowApp', () => {
await createComponent({ stubs });
expect(findJobCountBadge().text()).toBe('3');
- expect(findRunnersJobs().props('runner')).toEqual({ ...mockRunner, ...runner });
+ });
+ });
+
+ describe('When navigating to another tab', () => {
+ let routerPush;
+
+ beforeEach(async () => {
+ mockRunnerQueryResult();
+
+ await createComponent({ mountFn: mountExtended });
+
+ routerPush = jest.spyOn(wrapper.vm.$router, 'push').mockImplementation(() => {});
+ });
+
+ it('navigates to details', () => {
+ findTabAt(0).vm.$emit('click');
+ expect(routerPush).toHaveBeenLastCalledWith({ name: 'details' });
+ });
+
+ it('navigates to job', () => {
+ findTabAt(1).vm.$emit('click');
+ expect(routerPush).toHaveBeenLastCalledWith({ name: 'jobs' });
});
});
});
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 9778a6fe66c..9084ecdb4cc 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
@@ -25,6 +25,7 @@ import RunnerStats from '~/ci/runner/components/stat/runner_stats.vue';
import RunnerActionsCell from '~/ci/runner/components/cells/runner_actions_cell.vue';
import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
+import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue';
import {
ADMIN_FILTERED_SEARCH_NAMESPACE,
@@ -77,7 +78,9 @@ jest.mock('~/lib/utils/url_utility', () => ({
Vue.use(VueApollo);
Vue.use(GlToast);
-const COUNT_QUERIES = 7; // 4 tabs + 3 status queries
+const STATUS_COUNT_QUERIES = 3;
+const TAB_COUNT_QUERIES = 4;
+const COUNT_QUERIES = TAB_COUNT_QUERIES + STATUS_COUNT_QUERIES;
describe('AdminRunnersApp', () => {
let wrapper;
@@ -170,6 +173,29 @@ describe('AdminRunnersApp', () => {
});
});
+ describe('does not show total runner counts when total is 0', () => {
+ beforeEach(async () => {
+ mockRunnersCountHandler.mockResolvedValue({
+ data: {
+ runners: {
+ count: 0,
+ ...runnersCountData.runners,
+ },
+ },
+ });
+
+ await createComponent({ mountFn: mountExtended });
+ });
+
+ it('fetches only tab counts', () => {
+ expect(mockRunnersCountHandler).toHaveBeenCalledTimes(TAB_COUNT_QUERIES);
+ });
+
+ it('does not shows counters', () => {
+ expect(findRunnerStats().text()).toBe('');
+ });
+ });
+
it('shows the runners list', async () => {
await createComponent();
@@ -252,6 +278,15 @@ describe('AdminRunnersApp', () => {
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`);
});
+ it('Shows job status and links to jobs', () => {
+ const badge = wrapper
+ .find('tr [data-testid="td-summary"]')
+ .findComponent(RunnerJobStatusBadge);
+
+ expect(badge.props('jobStatus')).toBe(mockRunners[0].jobExecutionStatus);
+ expect(badge.attributes('href')).toBe(`http://localhost/admin/runners/${id}#/jobs`);
+ });
+
it('When runner is paused or unpaused, some data is refetched', async () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
diff --git a/spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
index 4aa354f9b62..10280c77303 100644
--- a/spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -1,12 +1,18 @@
import { __ } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerStackedSummaryCell from '~/ci/runner/components/cells/runner_stacked_summary_cell.vue';
+import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
+import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.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 } from '~/ci/runner/constants';
+import {
+ INSTANCE_TYPE,
+ I18N_INSTANCE_TYPE,
+ PROJECT_TYPE,
+ I18N_NO_DESCRIPTION,
+} from '~/ci/runner/constants';
import { allRunnersData } from '../../mock_data';
@@ -16,13 +22,14 @@ describe('RunnerTypeCell', () => {
let wrapper;
const findLockIcon = () => wrapper.findByTestId('lock-icon');
+ const findRunnerJobStatusBadge = () => wrapper.findComponent(RunnerJobStatusBadge);
const findRunnerTags = () => wrapper.findComponent(RunnerTags);
const findRunnerSummaryField = (icon) =>
wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon)
.wrappers[0];
const createComponent = (runner, options) => {
- wrapper = mountExtended(RunnerStackedSummaryCell, {
+ wrapper = mountExtended(RunnerSummaryCell, {
propsData: {
runner: {
...mockRunner,
@@ -80,6 +87,18 @@ describe('RunnerTypeCell', () => {
expect(wrapper.text()).toContain(mockRunner.description);
});
+ it('Displays the no runner description', () => {
+ createComponent({
+ description: null,
+ });
+
+ expect(wrapper.text()).toContain(I18N_NO_DESCRIPTION);
+ });
+
+ it('Displays job execution status', () => {
+ expect(findRunnerJobStatusBadge().props('jobStatus')).toBe(mockRunner.jobExecutionStatus);
+ });
+
it('Displays last contact', () => {
createComponent({
contactedAt: '2022-01-02',
@@ -147,14 +166,14 @@ describe('RunnerTypeCell', () => {
expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']);
});
- it('Displays a custom slot', () => {
+ it.each(['runner-name', 'runner-job-status-badge'])('Displays a custom "%s" slot', (slotName) => {
const slotContent = 'My custom runner name';
createComponent(
{},
{
slots: {
- 'runner-name': slotContent,
+ [slotName]: slotContent,
},
},
);
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 496c144083e..408750e646f 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
@@ -13,6 +13,7 @@ import {
DEFAULT_SORT,
CONTACTED_DESC,
} from '~/ci/runner/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -34,7 +35,7 @@ describe('RunnerList', () => {
const mockOtherSort = CONTACTED_DESC;
const mockFilters = [
{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
- { type: 'filtered-search-term', value: { data: '' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: '' } },
];
const expectToHaveLastEmittedInput = (value) => {
diff --git a/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
new file mode 100644
index 00000000000..015bebf40e3
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
@@ -0,0 +1,51 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue';
+import {
+ I18N_JOB_STATUS_RUNNING,
+ I18N_JOB_STATUS_IDLE,
+ JOB_STATUS_RUNNING,
+ JOB_STATUS_IDLE,
+} from '~/ci/runner/constants';
+
+describe('RunnerTypeBadge', () => {
+ let wrapper;
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+
+ const createComponent = ({ props, ...options } = {}) => {
+ wrapper = shallowMount(RunnerJobStatusBadge, {
+ propsData: {
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ it.each`
+ jobStatus | classes | text
+ ${JOB_STATUS_RUNNING} | ${['gl-mr-3', 'gl-bg-transparent!', 'gl-text-blue-600!', 'gl-border', 'gl-border-blue-600!']} | ${I18N_JOB_STATUS_RUNNING}
+ ${JOB_STATUS_IDLE} | ${['gl-mr-3', 'gl-bg-transparent!', 'gl-text-gray-700!', 'gl-border', 'gl-border-gray-500!']} | ${I18N_JOB_STATUS_IDLE}
+ `(
+ 'renders $jobStatus job status with "$text" text and styles',
+ ({ jobStatus, classes, text }) => {
+ createComponent({ props: { jobStatus } });
+
+ expect(findBadge().props()).toMatchObject({ size: 'sm', variant: 'muted' });
+ expect(findBadge().classes().sort()).toEqual(classes.sort());
+ expect(findBadge().text()).toBe(text);
+ },
+ );
+
+ it('does not render an unknown status', () => {
+ createComponent({ props: { jobStatus: 'UNKNOWN_STATUS' } });
+
+ expect(wrapper.html()).toBe('');
+ });
+
+ it('adds arbitrary attributes', () => {
+ createComponent({ props: { jobStatus: JOB_STATUS_RUNNING }, attrs: { href: '/url' } });
+
+ expect(findBadge().attributes('href')).toBe('/url');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index d53a0ce8f4f..1267d045623 100644
--- a/spec/frontend/ci/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -188,6 +188,21 @@ describe('RunnerList', () => {
expect(findCell({ fieldKey: 'summary' }).text()).toContain(`Summary: ${mockRunners[0].id}`);
});
+ it('Render #runner-job-status-badge slot in "summary" cell', () => {
+ createComponent(
+ {
+ scopedSlots: {
+ 'runner-job-status-badge': ({ runner }) => `Job status ${runner.jobExecutionStatus}`,
+ },
+ },
+ mountExtended,
+ );
+
+ expect(findCell({ fieldKey: 'summary' }).text()).toContain(
+ `Job status ${mockRunners[0].jobExecutionStatus}`,
+ );
+ });
+
it('Render #runner-actions-cell slot in "actions" cell', () => {
createComponent(
{
diff --git a/spec/frontend/ci/runner/components/runner_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
index 7d3064c2aef..45b410df2d4 100644
--- a/spec/frontend/ci/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
@@ -37,12 +37,12 @@ describe('RunnerTypeBadge', () => {
};
beforeEach(() => {
- jest.useFakeTimers('modern');
+ jest.useFakeTimers({ legacyFakeTimers: false });
jest.setSystemTime(new Date('2021-01-01T00:00:00Z'));
});
afterEach(() => {
- jest.useFakeTimers('legacy');
+ jest.useFakeTimers({ legacyFakeTimers: true });
wrapper.destroy();
});
diff --git a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
index d3c7ea50f9d..3dce5a509ca 100644
--- a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
+++ b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
@@ -7,7 +7,7 @@ import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import TagToken, { TAG_SUGGESTIONS_PATH } from '~/ci/runner/components/search_tokens/tag_token.vue';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
jest.mock('~/flash');
@@ -42,7 +42,7 @@ const mockTagTokenConfig = {
type: 'tag',
token: TagToken,
recentSuggestionsStorageKey: mockStorageKey,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
};
describe('TagToken', () => {
diff --git a/spec/frontend/ci/runner/components/stat/runner_stats_spec.js b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
index daebf3df050..3d45674d106 100644
--- a/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
@@ -16,6 +16,23 @@ describe('RunnerStats', () => {
const findSingleStats = () => wrapper.findAllComponents(RunnerSingleStat);
+ const RunnerCountStub = {
+ props: ['variables'],
+ render() {
+ // return a count for each status
+ const mockCounts = {
+ undefined: 6, // no status returns "all"
+ [STATUS_ONLINE]: 3,
+ [STATUS_OFFLINE]: 2,
+ [STATUS_STALE]: 1,
+ };
+
+ return this.$scopedSlots.default({
+ count: mockCounts[this.variables.status],
+ });
+ },
+ };
+
const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(RunnerStats, {
propsData: {
@@ -23,6 +40,9 @@ describe('RunnerStats', () => {
variables: {},
...props,
},
+ stubs: {
+ RunnerCount: RunnerCountStub,
+ },
...options,
});
};
@@ -32,24 +52,8 @@ describe('RunnerStats', () => {
});
it('Displays all the stats', () => {
- const mockCounts = {
- [STATUS_ONLINE]: 3,
- [STATUS_OFFLINE]: 2,
- [STATUS_STALE]: 1,
- };
-
createComponent({
mountFn: mount,
- stubs: {
- RunnerCount: {
- props: ['variables'],
- render() {
- return this.$scopedSlots.default({
- count: mockCounts[this.variables.status],
- });
- },
- },
- },
});
const text = wrapper.text();
@@ -78,4 +82,21 @@ describe('RunnerStats', () => {
expect(stat.props('variables')).toMatchObject(mockVariables);
});
});
+
+ it('Does not display counts when total is 0', () => {
+ createComponent({
+ mountFn: mount,
+ stubs: {
+ RunnerCount: {
+ render() {
+ return this.$scopedSlots.default({
+ count: 0,
+ });
+ },
+ },
+ },
+ });
+
+ expect(wrapper.html()).toBe('');
+ });
});
diff --git a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
index c3493b3c9fd..1e5bb828dbf 100644
--- a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
@@ -448,13 +448,15 @@ describe('GroupRunnersApp', () => {
it('navigates to the next page', async () => {
await findRunnerPaginationNext().trigger('click');
- expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith({
- groupFullPath: mockGroupFullPath,
- membership: MEMBERSHIP_DESCENDANTS,
- sort: CREATED_DESC,
- first: RUNNER_PAGE_SIZE,
- after: pageInfo.endCursor,
- });
+ expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ groupFullPath: mockGroupFullPath,
+ membership: MEMBERSHIP_DESCENDANTS,
+ sort: CREATED_DESC,
+ first: RUNNER_PAGE_SIZE,
+ after: pageInfo.endCursor,
+ }),
+ );
});
});
diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index eff5abc21b5..525756ed513 100644
--- a/spec/frontend/ci/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -18,6 +18,7 @@ import groupRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/grou
import groupRunnersCountData from 'test_fixtures/graphql/ci/runner/list/group_runners_count.query.graphql.json';
import { DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE } from '~/ci/runner/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
const emptyPageInfo = {
__typename: 'PageInfo',
@@ -73,7 +74,7 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: 'something' },
},
],
@@ -95,11 +96,11 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: 'something' },
},
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: 'else' },
},
],
diff --git a/spec/frontend/ci/runner/runner_search_utils_spec.js b/spec/frontend/ci/runner/runner_search_utils_spec.js
index 1db8fa1829b..f64b89d47fd 100644
--- a/spec/frontend/ci/runner/runner_search_utils_spec.js
+++ b/spec/frontend/ci/runner/runner_search_utils_spec.js
@@ -6,6 +6,7 @@ import {
fromSearchToVariables,
isSearchFiltered,
} from 'ee_else_ce/ci/runner/runner_search_utils';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import { mockSearchExamples } from './mock_data';
describe('search_params.js', () => {
@@ -48,8 +49,8 @@ describe('search_params.js', () => {
it('When search params appear as array, they are concatenated', () => {
expect(fromUrlQueryToSearch('?search[]=my&search[]=text').filters).toEqual([
- { type: 'filtered-search-term', value: { data: 'my' } },
- { type: 'filtered-search-term', value: { data: 'text' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'my' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'text' } },
]);
});
});
@@ -64,12 +65,13 @@ describe('search_params.js', () => {
it.each([
'http://test.host/?status[]=ACTIVE',
'http://test.host/?runner_type[]=INSTANCE_TYPE',
+ 'http://test.host/?paused[]=true',
'http://test.host/?search=my_text',
- ])('When a filter is removed, it is removed from the URL', (initalUrl) => {
+ ])('When a filter is removed, it is removed from the URL', (initialUrl) => {
const search = { filters: [], sort: 'CREATED_DESC' };
const expectedUrl = `http://test.host/`;
- expect(fromSearchToUrl(search, initalUrl)).toBe(expectedUrl);
+ expect(fromSearchToUrl(search, initialUrl)).toBe(expectedUrl);
});
it('When unrelated search parameter is present, it does not get removed', () => {
@@ -93,7 +95,7 @@ describe('search_params.js', () => {
fromSearchToVariables({
filters: [
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: '' },
},
],
@@ -106,11 +108,11 @@ describe('search_params.js', () => {
fromSearchToVariables({
filters: [
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: 'something' },
},
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: '' },
},
],
diff --git a/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
index c7375acd8e5..aa83638773d 100644
--- a/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
@@ -24,6 +24,7 @@ describe('Ci Project Variable wrapper', () => {
expect(findCiShared().props()).toEqual({
areScopedVariablesAvailable: false,
componentName: 'InstanceVariables',
+ entity: '',
hideEnvironmentScope: true,
mutationData: wrapper.vm.$options.mutationData,
queryData: wrapper.vm.$options.queryData,
diff --git a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
index ef5a86ccb61..ef624d8e4b4 100644
--- a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
@@ -39,6 +39,7 @@ describe('Ci Group Variable wrapper', () => {
id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, mockProvide.groupId),
areScopedVariablesAvailable: false,
componentName: 'GroupVariables',
+ entity: 'group',
fullPath: mockProvide.groupPath,
hideEnvironmentScope: false,
mutationData: wrapper.vm.$options.mutationData,
diff --git a/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
index 97051325f59..53c25e430f2 100644
--- a/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
@@ -35,6 +35,7 @@ describe('Ci Project Variable wrapper', () => {
id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, mockProvide.projectId),
areScopedVariablesAvailable: true,
componentName: 'ProjectVariables',
+ entity: 'project',
fullPath: mockProvide.projectFullPath,
hideEnvironmentScope: false,
mutationData: wrapper.vm.$options.mutationData,
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
index e4771f040d1..d177e755591 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
@@ -68,6 +68,7 @@ describe('Ci variable modal', () => {
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');
@@ -75,6 +76,7 @@ describe('Ci variable modal', () => {
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');
@@ -188,7 +190,7 @@ describe('Ci variable modal', () => {
});
});
- describe('Reference warning when adding a variable', () => {
+ describe('when expanded', () => {
describe('with a $ character', () => {
beforeEach(() => {
const [variable] = mockVariables;
@@ -205,6 +207,10 @@ describe('Ci variable modal', () => {
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', () => {
@@ -219,6 +225,73 @@ describe('Ci variable modal', () => {
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 } });
+ jest.spyOn(wrapper.vm, '$emit');
+
+ 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,
+ },
+ });
+ jest.spyOn(wrapper.vm, '$emit');
+
+ 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);
+ });
});
});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
index 8b5a0f7ae9d..5e459ee390f 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
@@ -17,40 +17,45 @@ describe('Ci variable table', () => {
const defaultProps = {
areScopedVariablesAvailable: true,
+ entity: 'project',
environments: mapEnvironmentNames(mockEnvs),
hideEnvironmentScope: false,
isLoading: false,
+ maxVariableLimit: 5,
variables: mockVariablesWithScopes(projectString),
};
const findCiVariableTable = () => wrapper.findComponent(ciVariableTable);
const findCiVariableModal = () => wrapper.findComponent(ciVariableModal);
- const createComponent = () => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(CiVariableSettings, {
propsData: {
...defaultProps,
+ ...props,
},
});
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
describe('props passing', () => {
it('passes props down correctly to the ci table', () => {
+ createComponent();
+
expect(findCiVariableTable().props()).toEqual({
+ entity: 'project',
isLoading: defaultProps.isLoading,
+ maxVariableLimit: defaultProps.maxVariableLimit,
variables: defaultProps.variables,
});
});
it('passes props down correctly to the ci modal', async () => {
+ createComponent();
+
findCiVariableTable().vm.$emit('set-selected-variable');
await nextTick();
@@ -66,6 +71,10 @@ describe('Ci variable table', () => {
});
describe('modal mode', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('passes down ADD mode when receiving an empty variable', async () => {
findCiVariableTable().vm.$emit('set-selected-variable');
await nextTick();
@@ -82,6 +91,10 @@ describe('Ci variable table', () => {
});
describe('variable modal', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('is hidden by default', () => {
expect(findCiVariableModal().exists()).toBe(false);
});
@@ -112,6 +125,10 @@ describe('Ci variable table', () => {
});
describe('variable events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it.each`
eventName
${'add-variable'}
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js
index 0cc0ee7a9c7..65a58a1647f 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js
@@ -29,6 +29,8 @@ import {
createGroupProps,
createInstanceProps,
createProjectProps,
+ createGroupProvide,
+ createProjectProvide,
devName,
mockProjectEnvironments,
mockProjectVariables,
@@ -44,6 +46,8 @@ Vue.use(VueApollo);
const mockProvide = {
endpoint: '/variables',
+ isGroup: false,
+ isProject: false,
};
const defaultProps = {
@@ -68,6 +72,7 @@ describe('Ci Variable Shared Component', () => {
customHandlers = null,
isLoading = false,
props = { ...createProjectProps() },
+ provide = {},
} = {}) {
const handlers = customHandlers || [
[getProjectEnvironments, mockEnvironments],
@@ -81,7 +86,10 @@ describe('Ci Variable Shared Component', () => {
...defaultProps,
...props,
},
- provide: mockProvide,
+ provide: {
+ ...mockProvide,
+ ...provide,
+ },
apolloProvider: mockApollo,
stubs: { ciVariableSettings, ciVariableTable },
});
@@ -108,12 +116,18 @@ describe('Ci Variable Shared Component', () => {
});
describe('when queries are resolved', () => {
- describe('successfuly', () => {
+ describe('successfully', () => {
beforeEach(async () => {
mockEnvironments.mockResolvedValue(mockProjectEnvironments);
mockVariables.mockResolvedValue(mockProjectVariables);
- await createComponentWithApollo();
+ await createComponentWithApollo({ provide: createProjectProvide() });
+ });
+
+ it('passes down the expected max variable limit as props', () => {
+ expect(findCiSettings().props('maxVariableLimit')).toBe(
+ mockProjectVariables.data.project.ciVariables.limit,
+ );
});
it('passes down the expected environments as props', () => {
@@ -285,23 +299,29 @@ describe('Ci Variable Shared Component', () => {
});
describe('Props', () => {
+ const mockGroupCiVariables = mockGroupVariables.data.group.ciVariables;
+ const mockProjectCiVariables = mockProjectVariables.data.project.ciVariables;
+
describe('in a specific context as', () => {
it.each`
- name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | mutation
- ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${null}
- ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${getGroupVariables}
- ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${getAdminVariables}
+ name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | provideFn | mutation | maxVariableLimit
+ ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${createProjectProvide} | ${null} | ${mockProjectCiVariables.limit}
+ ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${createGroupProvide} | ${getGroupVariables} | ${mockGroupCiVariables.limit}
+ ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${() => {}} | ${getAdminVariables} | ${0}
`(
'passes down all the required props when its a $name component',
async ({
mutation,
+ maxVariableLimit,
mockVariablesValue,
mockEnvironmentsValue,
withEnvironments,
expectedEnvironments,
propsFn,
+ provideFn,
}) => {
const props = propsFn();
+ const provide = provideFn();
mockVariables.mockResolvedValue(mockVariablesValue);
@@ -315,13 +335,15 @@ describe('Ci Variable Shared Component', () => {
customHandlers = [[mutation, mockVariables]];
}
- await createComponentWithApollo({ customHandlers, props });
+ await createComponentWithApollo({ customHandlers, props, provide });
expect(findCiSettings().props()).toEqual({
areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
isLoading: false,
+ maxVariableLimit,
variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data)?.nodes,
+ entity: props.entity,
environments: expectedEnvironments,
});
},
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
index 8a4c35173ec..9891bc397b6 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
@@ -1,16 +1,22 @@
+import { GlAlert } from '@gitlab/ui';
+import { sprintf } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
-import { projectString } from '~/ci_variable_list/constants';
+import { EXCEEDS_VARIABLE_LIMIT_TEXT, projectString } from '~/ci_variable_list/constants';
import { mockVariables } from '../mocks';
describe('Ci variable table', () => {
let wrapper;
const defaultProps = {
+ entity: 'project',
isLoading: false,
+ maxVariableLimit: mockVariables(projectString).length + 1,
variables: mockVariables(projectString),
};
+ const mockMaxVariableLimit = defaultProps.variables.length;
+
const createComponent = ({ props = {} } = {}) => {
wrapper = mountExtended(CiVariableTable, {
attachTo: document.body,
@@ -25,8 +31,15 @@ describe('Ci variable table', () => {
const findAddButton = () => wrapper.findByLabelText('Add');
const findEditButton = () => wrapper.findByLabelText('Edit');
const findEmptyVariablesPlaceholder = () => wrapper.findByText('There are no variables yet.');
- const findHiddenValues = () => wrapper.findAll('[data-testid="hiddenValue"]');
- const findRevealedValues = () => wrapper.findAll('[data-testid="revealedValue"]');
+ const findHiddenValues = () => wrapper.findAllByTestId('hiddenValue');
+ const findLimitReachedAlerts = () => wrapper.findAllComponents(GlAlert);
+ const findRevealedValues = () => wrapper.findAllByTestId('revealedValue');
+ const findOptionsValues = (rowIndex) =>
+ wrapper.findAllByTestId('ci-variable-table-row-options').at(rowIndex).text();
+
+ const generateExceedsVariableLimitText = (entity, currentVariableCount, maxVariableLimit) => {
+ return sprintf(EXCEEDS_VARIABLE_LIMIT_TEXT, { entity, currentVariableCount, maxVariableLimit });
+ };
beforeEach(() => {
createComponent();
@@ -66,6 +79,67 @@ describe('Ci variable table', () => {
it('displays the correct amount of variables', async () => {
expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length);
});
+
+ it('displays the correct variable options', async () => {
+ expect(findOptionsValues(0)).toBe('Protected, Expanded');
+ expect(findOptionsValues(1)).toBe('Masked');
+ });
+
+ it('enables the Add Variable button', () => {
+ expect(findAddButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe('When variables have exceeded the max limit', () => {
+ beforeEach(() => {
+ createComponent({ props: { maxVariableLimit: mockVariables(projectString).length } });
+ });
+
+ it('disables the Add Variable button', () => {
+ expect(findAddButton().props('disabled')).toBe(true);
+ });
+ });
+
+ describe('max limit reached alert', () => {
+ describe('when there is no variable limit', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { maxVariableLimit: 0 },
+ });
+ });
+
+ it('hides alert', () => {
+ expect(findLimitReachedAlerts().length).toBe(0);
+ });
+ });
+
+ describe('when variable limit exists', () => {
+ it('hides alert when limit has not been reached', () => {
+ createComponent();
+
+ expect(findLimitReachedAlerts().length).toBe(0);
+ });
+
+ it('shows alert when limit has been reached', () => {
+ const exceedsVariableLimitText = generateExceedsVariableLimitText(
+ defaultProps.entity,
+ defaultProps.variables.length,
+ mockMaxVariableLimit,
+ );
+
+ createComponent({
+ props: { maxVariableLimit: mockMaxVariableLimit },
+ });
+
+ expect(findLimitReachedAlerts().length).toBe(2);
+
+ expect(findLimitReachedAlerts().at(0).props('dismissible')).toBe(false);
+ expect(findLimitReachedAlerts().at(0).text()).toContain(exceedsVariableLimitText);
+
+ expect(findLimitReachedAlerts().at(1).props('dismissible')).toBe(false);
+ expect(findLimitReachedAlerts().at(1).text()).toContain(exceedsVariableLimitText);
+ });
+ });
});
describe('Table click actions', () => {
diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js
index 03b77f80430..065e9fa6667 100644
--- a/spec/frontend/ci_variable_list/mocks.js
+++ b/spec/frontend/ci_variable_list/mocks.js
@@ -34,6 +34,7 @@ export const mockVariables = (kind) => {
key: 'my-var',
masked: false,
protected: true,
+ raw: false,
value: 'variable_value',
variableType: variableTypes.envType,
},
@@ -43,6 +44,7 @@ export const mockVariables = (kind) => {
key: 'secret',
masked: true,
protected: false,
+ raw: true,
value: 'another_value',
variableType: variableTypes.fileType,
},
@@ -63,6 +65,7 @@ const createDefaultVars = ({ withScope = true, kind } = {}) => {
return {
__typename: `Ci${kind}VariableConnection`,
+ limit: 200,
pageInfo: {
startCursor: 'adsjsd12kldpsa',
endCursor: 'adsjsd12kldpsa',
@@ -140,6 +143,7 @@ export const newVariable = {
export const createProjectProps = () => {
return {
componentName: 'ProjectVariable',
+ entity: 'project',
fullPath: '/namespace/project/',
id: 'gid://gitlab/Project/20',
mutationData: {
@@ -163,6 +167,7 @@ export const createProjectProps = () => {
export const createGroupProps = () => {
return {
componentName: 'GroupVariable',
+ entity: 'group',
fullPath: '/my-group',
id: 'gid://gitlab/Group/20',
mutationData: {
@@ -182,6 +187,7 @@ export const createGroupProps = () => {
export const createInstanceProps = () => {
return {
componentName: 'InstanceVariable',
+ entity: '',
mutationData: {
[ADD_MUTATION_ACTION]: addAdminVariable,
[UPDATE_MUTATION_ACTION]: updateAdminVariable,
@@ -195,3 +201,13 @@ export const createInstanceProps = () => {
},
};
};
+
+export const createGroupProvide = () => ({
+ isGroup: true,
+ isProject: false,
+});
+
+export const createProjectProvide = () => ({
+ isGroup: false,
+ isProject: true,
+});
diff --git a/spec/frontend/clusters_list/components/agent_token_spec.js b/spec/frontend/clusters_list/components/agent_token_spec.js
index 8d3130b45a6..a92a03fedb6 100644
--- a/spec/frontend/clusters_list/components/agent_token_spec.js
+++ b/spec/frontend/clusters_list/components/agent_token_spec.js
@@ -1,7 +1,13 @@
-import { GlAlert, GlFormInputGroup } from '@gitlab/ui';
+import { GlAlert, GlFormInputGroup, GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { sprintf } from '~/locale';
import AgentToken from '~/clusters_list/components/agent_token.vue';
-import { I18N_AGENT_TOKEN, INSTALL_AGENT_MODAL_ID } from '~/clusters_list/constants';
+import {
+ I18N_AGENT_TOKEN,
+ INSTALL_AGENT_MODAL_ID,
+ NAME_MAX_LENGTH,
+ HELM_VERSION_POLICY_URL,
+} from '~/clusters_list/constants';
import { generateAgentRegistrationCommand } from '~/clusters_list/clusters_util';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@@ -19,15 +25,17 @@ describe('InstallAgentModal', () => {
const findCodeBlock = () => wrapper.findComponent(CodeBlock);
const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
const findInput = () => wrapper.findComponent(GlFormInputGroup);
+ const findHelmVersionPolicyLink = () => wrapper.findComponent(GlLink);
+ const findHelmExternalLinkIcon = () => wrapper.findComponent(GlIcon);
- const createWrapper = () => {
+ const createWrapper = (newAgentName = agentName) => {
const provide = {
kasAddress,
kasVersion,
};
const propsData = {
- agentName,
+ agentName: newAgentName,
agentToken,
modalId,
};
@@ -35,6 +43,9 @@ describe('InstallAgentModal', () => {
wrapper = shallowMountExtended(AgentToken, {
provide,
propsData,
+ stubs: {
+ GlSprintf,
+ },
});
};
@@ -52,6 +63,17 @@ describe('InstallAgentModal', () => {
expect(wrapper.text()).toContain(I18N_AGENT_TOKEN.basicInstallBody);
});
+ it('shows Helm version policy text with an external link', () => {
+ expect(wrapper.text()).toContain(
+ sprintf(I18N_AGENT_TOKEN.helmVersionText, { linkStart: '', linkEnd: ' ' }),
+ );
+ expect(findHelmVersionPolicyLink().attributes()).toMatchObject({
+ href: HELM_VERSION_POLICY_URL,
+ target: '_blank',
+ });
+ expect(findHelmExternalLinkIcon().props()).toMatchObject({ name: 'external-link', size: 12 });
+ });
+
it('shows advanced agent installation instructions', () => {
expect(wrapper.text()).toContain(I18N_AGENT_TOKEN.advancedInstallTitle);
});
@@ -79,9 +101,19 @@ describe('InstallAgentModal', () => {
it('shows code block with agent installation command', () => {
expect(findCodeBlock().props('code')).toContain(`helm upgrade --install ${agentName}`);
+ expect(findCodeBlock().props('code')).toContain(`--namespace gitlab-agent-${agentName}`);
expect(findCodeBlock().props('code')).toContain(`--set config.token=${agentToken}`);
expect(findCodeBlock().props('code')).toContain(`--set config.kasAddress=${kasAddress}`);
expect(findCodeBlock().props('code')).toContain(`--set image.tag=v${kasVersion}`);
});
+
+ it('truncates the namespace name if it exceeds the maximum length', () => {
+ const newAgentName = 'agent-name-that-is-too-long-and-needs-to-be-truncated-to-use';
+ createWrapper(newAgentName);
+
+ expect(findCodeBlock().props('code')).toContain(
+ `--namespace gitlab-agent-${newAgentName.substring(0, NAME_MAX_LENGTH)}`,
+ );
+ });
});
});
diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js
index bff1e573dbd..2372ab30300 100644
--- a/spec/frontend/clusters_list/components/agents_spec.js
+++ b/spec/frontend/clusters_list/components/agents_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlKeysetPagination, GlLoadingIcon, GlBanner } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
import AgentEmptyState from '~/clusters_list/components/agent_empty_state.vue';
import AgentTable from '~/clusters_list/components/agent_table.vue';
import Agents from '~/clusters_list/components/agents.vue';
@@ -12,10 +12,10 @@ import {
} from '~/clusters_list/constants';
import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('Agents', () => {
let wrapper;
@@ -34,9 +34,10 @@ describe('Agents', () => {
pageInfo = null,
trees = [],
count = 0,
+ queryResponse = null,
}) => {
const provide = provideData;
- const apolloQueryResponse = {
+ const queryResponseData = {
data: {
project: {
id: '1',
@@ -51,13 +52,12 @@ describe('Agents', () => {
},
},
};
+ const agentQueryResponse =
+ queryResponse || jest.fn().mockResolvedValue(queryResponseData, provide);
- const apolloProvider = createMockApollo([
- [getAgentsQuery, jest.fn().mockResolvedValue(apolloQueryResponse, provide)],
- ]);
+ const apolloProvider = createMockApollo([[getAgentsQuery, agentQueryResponse]]);
wrapper = shallowMount(Agents, {
- localVue,
apolloProvider,
propsData: {
...defaultProps,
@@ -313,24 +313,11 @@ describe('Agents', () => {
});
describe('when agents query is loading', () => {
- const mocks = {
- $apollo: {
- queries: {
- agents: {
- loading: true,
- },
- },
- },
- };
-
- beforeEach(async () => {
- wrapper = shallowMount(Agents, {
- mocks,
- propsData: defaultProps,
- provide: provideData,
+ beforeEach(() => {
+ createWrapper({
+ queryResponse: jest.fn().mockReturnValue(new Promise(() => {})),
});
-
- await nextTick();
+ return waitForPromises();
});
it('displays a loading icon', () => {
diff --git a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
index 197735d3c77..02b455d0b61 100644
--- a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
+++ b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
@@ -1,34 +1,31 @@
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlButton } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { ENTER_KEY } from '~/lib/utils/keys';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '~/clusters_list/constants';
describe('AvailableAgentsDropdown', () => {
let wrapper;
+ const configuredAgent = 'configured-agent';
+ const searchAgentName = 'search-agent';
+ const newAgentName = 'new-agent';
+
const i18n = I18N_AVAILABLE_AGENTS_DROPDOWN;
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findFirstAgentItem = () => findDropdownItems().at(0);
- const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
- const findCreateButton = () => wrapper.findByTestId('create-config-button');
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findCreateButton = () => wrapper.findComponent(GlButton);
const createWrapper = ({ propsData }) => {
wrapper = shallowMountExtended(AvailableAgentsDropdown, {
propsData,
- stubs: { GlDropdown },
+ stubs: { GlCollapsibleListbox },
});
- wrapper.vm.$refs.dropdown.hide = jest.fn();
+ wrapper.vm.$refs.dropdown.closeAndFocus = jest.fn();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('there are agents available', () => {
const propsData = {
- availableAgents: ['configured-agent', 'search-agent', 'test-agent'],
+ availableAgents: [configuredAgent, searchAgentName, 'test-agent'],
isRegistering: false,
};
@@ -37,91 +34,93 @@ describe('AvailableAgentsDropdown', () => {
});
it('prompts to select an agent', () => {
- expect(findDropdown().props('text')).toBe(i18n.selectAgent);
+ expect(findDropdown().props('toggleText')).toBe(i18n.selectAgent);
});
describe('search agent', () => {
it('renders search button', () => {
- expect(findSearchInput().exists()).toBe(true);
+ expect(findDropdown().props('searchable')).toBe(true);
});
it('renders all agents when search term is empty', () => {
- expect(findDropdownItems()).toHaveLength(3);
+ expect(findDropdown().props('items')).toHaveLength(3);
});
it('renders only the agent searched for when the search item exists', async () => {
- await findSearchInput().vm.$emit('input', 'search-agent');
-
- expect(findDropdownItems()).toHaveLength(1);
- expect(findFirstAgentItem().text()).toBe('search-agent');
- });
+ findDropdown().vm.$emit('search', searchAgentName);
+ await nextTick();
- it('renders create button when search started', async () => {
- await findSearchInput().vm.$emit('input', 'new-agent');
-
- expect(findCreateButton().exists()).toBe(true);
+ expect(findDropdown().props('items')).toMatchObject([
+ { text: searchAgentName, value: searchAgentName },
+ ]);
});
- it("doesn't render create button when search item is found", async () => {
- await findSearchInput().vm.$emit('input', 'search-agent');
-
- expect(findCreateButton().exists()).toBe(false);
+ describe('create button', () => {
+ it.each`
+ condition | search | createButtonRendered
+ ${'is rendered'} | ${newAgentName} | ${true}
+ ${'is not rendered'} | ${''} | ${false}
+ ${'is not rendered'} | ${searchAgentName} | ${false}
+ `('$condition when search is "$search"', async ({ search, createButtonRendered }) => {
+ findDropdown().vm.$emit('search', search);
+ await nextTick();
+
+ expect(findCreateButton().exists()).toBe(createButtonRendered);
+ });
});
});
describe('select existing agent configuration', () => {
beforeEach(() => {
- findFirstAgentItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', configuredAgent);
});
- it('emits agentSelected with the name of the clicked agent', () => {
- expect(wrapper.emitted('agentSelected')).toEqual([['configured-agent']]);
+ it('emits `agentSelected` with the name of the clicked agent', () => {
+ expect(wrapper.emitted('agentSelected')).toEqual([[configuredAgent]]);
});
it('marks the clicked item as selected', () => {
- expect(findDropdown().props('text')).toBe('configured-agent');
- expect(findFirstAgentItem().props('isChecked')).toBe(true);
+ expect(findDropdown().props('toggleText')).toBe(configuredAgent);
});
});
describe('create new agent configuration', () => {
beforeEach(async () => {
- await findSearchInput().vm.$emit('input', 'new-agent');
+ findDropdown().vm.$emit('search', newAgentName);
+ await nextTick();
findCreateButton().vm.$emit('click');
});
it('emits agentSelected with the name of the clicked agent', () => {
- expect(wrapper.emitted('agentSelected')).toEqual([['new-agent']]);
+ expect(wrapper.emitted('agentSelected')).toEqual([[newAgentName]]);
});
it('marks the clicked item as selected', () => {
- expect(findDropdown().props('text')).toBe('new-agent');
+ expect(findDropdown().props('toggleText')).toBe(newAgentName);
});
});
describe('click enter to register new agent without configuration', () => {
beforeEach(async () => {
- await findSearchInput().vm.$emit('input', 'new-agent');
- await findSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+ const dropdown = findDropdown();
+ dropdown.vm.$emit('search', newAgentName);
+ await nextTick();
+ await dropdown.trigger('keydown.enter');
});
it('emits agentSelected with the name of the clicked agent', () => {
- expect(wrapper.emitted('agentSelected')).toEqual([['new-agent']]);
+ expect(wrapper.emitted('agentSelected')).toEqual([[newAgentName]]);
});
it('marks the clicked item as selected', () => {
- expect(findDropdown().props('text')).toBe('new-agent');
- });
-
- it('closes the dropdown', () => {
- expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1);
+ expect(findDropdown().props('toggleText')).toBe(newAgentName);
});
});
});
describe('registration in progress', () => {
const propsData = {
- availableAgents: ['configured-agent'],
+ availableAgents: [configuredAgent],
isRegistering: true,
};
@@ -130,7 +129,7 @@ describe('AvailableAgentsDropdown', () => {
});
it('updates the text in the dropdown', () => {
- expect(findDropdown().props('text')).toBe(i18n.registeringAgent);
+ expect(findDropdown().props('toggleText')).toBe(i18n.registeringAgent);
});
it('displays a loading icon', () => {
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index a3f42c1f161..e8e705a6384 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -61,6 +61,10 @@ describe('Clusters', () => {
let captureException;
beforeEach(() => {
+ jest.spyOn(Sentry, 'withScope').mockImplementation((fn) => {
+ const mockScope = { setTag: () => {} };
+ fn(mockScope);
+ });
captureException = jest.spyOn(Sentry, 'captureException');
mock = new MockAdapter(axios);
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index 09b1f80ff9b..1deebf8b75a 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -17,6 +17,10 @@ describe('Clusters store actions', () => {
describe('reportSentryError', () => {
beforeEach(() => {
+ jest.spyOn(Sentry, 'withScope').mockImplementation((fn) => {
+ const mockScope = { setTag: () => {} };
+ fn(mockScope);
+ });
captureException = jest.spyOn(Sentry, 'captureException');
});
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
index 6ad8a9de8d3..331a0a474a3 100644
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
@@ -14,15 +14,15 @@ exports[`content_editor/components/toolbar_link_button renders dropdown componen
</div>
</form>
</li>
- <li role=\\"presentation\\" class=\\"gl-new-dropdown-divider\\">
+ <li role=\\"presentation\\" class=\\"gl-dropdown-divider\\">
<hr role=\\"separator\\" aria-orientation=\\"horizontal\\" class=\\"dropdown-divider\\">
</li>
- <li role=\\"presentation\\" class=\\"gl-new-dropdown-item\\"><button role=\\"menuitem\\" type=\\"button\\" class=\\"dropdown-item\\">
+ <li role=\\"presentation\\" class=\\"gl-dropdown-item\\"><button role=\\"menuitem\\" type=\\"button\\" class=\\"dropdown-item\\">
<!---->
<!---->
<!---->
- <div class=\\"gl-new-dropdown-item-text-wrapper\\">
- <p class=\\"gl-new-dropdown-item-text-primary\\">
+ <div class=\\"gl-dropdown-item-text-wrapper\\">
+ <p class=\\"gl-dropdown-item-text-primary\\">
Upload file
</p>
<!---->
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index c1c2a125515..1a3cd36a8bb 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -10,7 +10,7 @@ import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/forma
import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block_bubble_menu.vue';
import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble_menu.vue';
import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue';
-import TopToolbar from '~/content_editor/components/top_toolbar.vue';
+import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vue';
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { KEYDOWN_EVENT } from '~/content_editor/constants';
@@ -27,13 +27,14 @@ describe('ContentEditor', () => {
const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator);
const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert);
- const createWrapper = ({ markdown, autofocus } = {}) => {
+ const createWrapper = ({ markdown, autofocus, useBottomToolbar } = {}) => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
renderMarkdown,
uploadsPath,
markdown,
autofocus,
+ useBottomToolbar,
},
stubs: {
EditorStateObserver,
@@ -89,7 +90,19 @@ describe('ContentEditor', () => {
it('renders top toolbar component', () => {
createWrapper();
- expect(wrapper.findComponent(TopToolbar).exists()).toBe(true);
+ expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
+ expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-t')).toBe(false);
+ expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-b')).toBe(true);
+ });
+
+ it('renders bottom toolbar component', () => {
+ createWrapper({
+ useBottomToolbar: true,
+ });
+
+ expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
+ expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-t')).toBe(true);
+ expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-b')).toBe(false);
});
describe('when setting initial content', () => {
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
index 8f194ff32e2..c4bf21ba813 100644
--- a/spec/frontend/content_editor/components/top_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
@@ -1,6 +1,6 @@
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import TopToolbar from '~/content_editor/components/top_toolbar.vue';
+import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vue';
import {
TOOLBAR_CONTROL_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
@@ -11,7 +11,7 @@ describe('content_editor/components/top_toolbar', () => {
let trackingSpy;
const buildWrapper = () => {
- wrapper = shallowMountExtended(TopToolbar);
+ wrapper = shallowMountExtended(FormattingToolbar);
};
beforeEach(() => {
diff --git a/spec/frontend/content_editor/extensions/comment_spec.js b/spec/frontend/content_editor/extensions/comment_spec.js
new file mode 100644
index 00000000000..7d8ff28e4d7
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/comment_spec.js
@@ -0,0 +1,30 @@
+import Comment from '~/content_editor/extensions/comment';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
+
+describe('content_editor/extensions/comment', () => {
+ let tiptapEditor;
+ let doc;
+ let comment;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Comment] });
+ ({
+ builders: { doc, comment },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ comment: { nodeType: Comment.name },
+ },
+ }));
+ });
+
+ describe('when typing the comment input rule', () => {
+ it('inserts a comment node', () => {
+ const expectedDoc = doc(comment());
+
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: '<!-- ' });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
index 5458a42532f..90d83820c70 100644
--- a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
+++ b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
@@ -1,5 +1,6 @@
import createMarkdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
import Bold from '~/content_editor/extensions/bold';
+import Comment from '~/content_editor/extensions/comment';
import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/services/gl_api_markdown_deserializer', () => {
@@ -7,19 +8,21 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
let doc;
let p;
let bold;
+ let comment;
let tiptapEditor;
beforeEach(() => {
tiptapEditor = createTestEditor({
- extensions: [Bold],
+ extensions: [Bold, Comment],
});
({
- builders: { doc, p, bold },
+ builders: { doc, p, bold, comment },
} = createDocBuilder({
tiptapEditor,
names: {
bold: { markType: Bold.name },
+ comment: { nodeType: Comment.name },
},
}));
renderMarkdown = jest.fn();
@@ -33,7 +36,7 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
renderMarkdown.mockResolvedValueOnce(
- `<p><strong>${text}</strong></p><pre lang="javascript"></pre>`,
+ `<p><strong>${text}</strong></p><pre lang="javascript"></pre><!-- some comment -->`,
);
result = await deserializer.deserialize({
@@ -41,8 +44,9 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
schema: tiptapEditor.schema,
});
});
+
it('transforms HTML returned by render function to a ProseMirror document', async () => {
- const document = doc(p(bold(text)));
+ const document = doc(p(bold(text)), comment(' some comment '));
expect(result.document.toJSON()).toEqual(document.toJSON());
});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 1bf23415052..2cd8b8a0d6f 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -3,6 +3,7 @@ import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
import Code from '~/content_editor/extensions/code';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import Comment from '~/content_editor/extensions/comment';
import DescriptionItem from '~/content_editor/extensions/description_item';
import DescriptionList from '~/content_editor/extensions/description_list';
import Details from '~/content_editor/extensions/details';
@@ -50,6 +51,7 @@ const {
bulletList,
code,
codeBlock,
+ comment,
details,
detailsContent,
div,
@@ -89,6 +91,7 @@ const {
bulletList: { nodeType: BulletList.name },
code: { markType: Code.name },
codeBlock: { nodeType: CodeBlockHighlight.name },
+ comment: { nodeType: Comment.name },
details: { nodeType: Details.name },
detailsContent: { nodeType: DetailsContent.name },
descriptionItem: { nodeType: DescriptionItem.name },
@@ -169,6 +172,17 @@ describe('markdownSerializer', () => {
);
});
+ it('correctly serializes a comment node', () => {
+ expect(serialize(paragraph('hi'), comment(' this is a\ncomment '))).toBe(
+ `
+hi
+
+<!-- this is a
+comment -->
+ `.trim(),
+ );
+ });
+
it('correctly serializes a line break', () => {
expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld');
});
@@ -304,7 +318,7 @@ var y = 10;
expect(
serialize(
codeBlock(
- { language: 'json' },
+ { language: 'json', langParams: '' },
'this is not really json but just trying out whether this case works or not',
),
),
@@ -317,6 +331,23 @@ this is not really json but just trying out whether this case works or not
);
});
+ it('correctly serializes a code block with language parameters', () => {
+ expect(
+ serialize(
+ codeBlock(
+ { language: 'json', langParams: 'table' },
+ 'this is not really json:table but just trying out whether this case works or not',
+ ),
+ ),
+ ).toBe(
+ `
+\`\`\`json:table
+this is not really json:table but just trying out whether this case works or not
+\`\`\`
+ `.trim(),
+ );
+ });
+
it('correctly serializes emoji', () => {
expect(serialize(paragraph(emoji({ name: 'dog' })))).toBe(':dog:');
});
@@ -366,6 +397,26 @@ this is not really json but just trying out whether this case works or not
);
});
+ it.each`
+ width | height | outputAttributes
+ ${300} | ${undefined} | ${'width=300'}
+ ${undefined} | ${300} | ${'height=300'}
+ ${300} | ${300} | ${'width=300 height=300'}
+ ${'300%'} | ${'300px'} | ${'width="300%" height="300px"'}
+ `(
+ 'correctly serializes an image with width and height attributes',
+ ({ width, height, outputAttributes }) => {
+ const imageAttrs = { src: 'img.jpg', alt: 'foo bar' };
+
+ if (width) imageAttrs.width = width;
+ if (height) imageAttrs.height = height;
+
+ expect(serialize(paragraph(image(imageAttrs)))).toBe(
+ `![foo bar](img.jpg){${outputAttributes}}`,
+ );
+ },
+ );
+
it('does not serialize an image when src and canonicalSrc are empty', () => {
expect(serialize(paragraph(image({})))).toBe('');
});
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 0768fa6e8df..0fa0e65cd26 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -11,10 +11,12 @@ import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
import Code from '~/content_editor/extensions/code';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import Comment from '~/content_editor/extensions/comment';
import DescriptionItem from '~/content_editor/extensions/description_item';
import DescriptionList from '~/content_editor/extensions/description_list';
import Details from '~/content_editor/extensions/details';
import DetailsContent from '~/content_editor/extensions/details_content';
+import Diagram from '~/content_editor/extensions/diagram';
import Emoji from '~/content_editor/extensions/emoji';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
import FootnoteReference from '~/content_editor/extensions/footnote_reference';
@@ -211,10 +213,12 @@ export const createTiptapEditor = (extensions = []) =>
BulletList,
Code,
CodeBlockHighlight,
+ Comment,
DescriptionItem,
DescriptionList,
Details,
DetailsContent,
+ Diagram,
Emoji,
FootnoteDefinition,
FootnoteReference,
diff --git a/spec/frontend/crm/contact_form_wrapper_spec.js b/spec/frontend/crm/contact_form_wrapper_spec.js
index e49b553e4b5..50b432943fb 100644
--- a/spec/frontend/crm/contact_form_wrapper_spec.js
+++ b/spec/frontend/crm/contact_form_wrapper_spec.js
@@ -4,7 +4,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import ContactFormWrapper from '~/crm/contacts/components/contact_form_wrapper.vue';
-import ContactForm from '~/crm/components/form.vue';
+import CrmForm from '~/crm/components/crm_form.vue';
import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql';
import createContactMutation from '~/crm/contacts/components/graphql/create_contact.mutation.graphql';
import updateContactMutation from '~/crm/contacts/components/graphql/update_contact.mutation.graphql';
@@ -16,7 +16,7 @@ describe('Customer relations contact form wrapper', () => {
let wrapper;
let fakeApollo;
- const findContactForm = () => wrapper.findComponent(ContactForm);
+ const findCrmForm = () => wrapper.findComponent(CrmForm);
const $route = {
params: {
@@ -65,21 +65,21 @@ describe('Customer relations contact form wrapper', () => {
});
it('renders correct getQuery prop', () => {
- expect(findContactForm().props('getQueryNodePath')).toBe('group.contacts');
+ expect(findCrmForm().props('getQueryNodePath')).toBe('group.contacts');
});
it('renders correct mutation prop', () => {
- expect(findContactForm().props('mutation')).toBe(mutation);
+ expect(findCrmForm().props('mutation')).toBe(mutation);
});
it('renders correct additionalCreateParams prop', () => {
- expect(findContactForm().props('additionalCreateParams')).toMatchObject({
+ expect(findCrmForm().props('additionalCreateParams')).toMatchObject({
groupId: 'gid://gitlab/Group/26',
});
});
it('renders correct existingId prop', () => {
- expect(findContactForm().props('existingId')).toBe(existingId);
+ expect(findCrmForm().props('existingId')).toBe(existingId);
});
it('renders correct fields prop', () => {
@@ -101,15 +101,15 @@ describe('Customer relations contact form wrapper', () => {
{ name: 'description', label: 'Description' },
];
if (isEditMode) fields.push({ name: 'active', label: 'Active', required: true, bool: true });
- expect(findContactForm().props('fields')).toEqual(fields);
+ expect(findCrmForm().props('fields')).toEqual(fields);
});
it('renders correct title prop', () => {
- expect(findContactForm().props('title')).toBe(title);
+ expect(findCrmForm().props('title')).toBe(title);
});
it('renders correct successMessage prop', () => {
- expect(findContactForm().props('successMessage')).toBe(successMessage);
+ expect(findCrmForm().props('successMessage')).toBe(successMessage);
});
});
});
diff --git a/spec/frontend/crm/form_spec.js b/spec/frontend/crm/crm_form_spec.js
index 57e28b396cf..eabcf5b1b1b 100644
--- a/spec/frontend/crm/form_spec.js
+++ b/spec/frontend/crm/crm_form_spec.js
@@ -5,7 +5,7 @@ import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import Form from '~/crm/components/form.vue';
+import CrmForm from '~/crm/components/crm_form.vue';
import routes from '~/crm/contacts/routes';
import createContactMutation from '~/crm/contacts/components/graphql/create_contact.mutation.graphql';
import updateContactMutation from '~/crm/contacts/components/graphql/update_contact.mutation.graphql';
@@ -81,7 +81,7 @@ describe('Reusable form component', () => {
const findFormGroup = (at) => wrapper.findAllComponents(GlFormGroup).at(at);
const mountComponent = (propsData) => {
- wrapper = shallowMountExtended(Form, {
+ wrapper = shallowMountExtended(CrmForm, {
router,
apolloProvider: fakeApollo,
propsData: { drawerOpen: true, ...propsData },
diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js
index 9f26b9157e6..d795c585622 100644
--- a/spec/frontend/crm/organization_form_wrapper_spec.js
+++ b/spec/frontend/crm/organization_form_wrapper_spec.js
@@ -1,6 +1,6 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import OrganizationFormWrapper from '~/crm/organizations/components/organization_form_wrapper.vue';
-import OrganizationForm from '~/crm/components/form.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_organization.mutation.graphql';
import updateOrganizationMutation from '~/crm/organizations/components/graphql/update_organization.mutation.graphql';
@@ -8,7 +8,7 @@ import updateOrganizationMutation from '~/crm/organizations/components/graphql/u
describe('Customer relations organization form wrapper', () => {
let wrapper;
- const findOrganizationForm = () => wrapper.findComponent(OrganizationForm);
+ const findOrganizationForm = () => wrapper.findComponent(CrmForm);
const $apollo = {
queries: {
diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js
index ce0c924bed2..9b96ce5d252 100644
--- a/spec/frontend/deploy_freeze/store/actions_spec.js
+++ b/spec/frontend/deploy_freeze/store/actions_spec.js
@@ -4,7 +4,7 @@ import Api from '~/api';
import * as actions from '~/deploy_freeze/store/actions';
import * as types from '~/deploy_freeze/store/mutation_types';
import getInitialState from '~/deploy_freeze/store/state';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
import { freezePeriodsFixture } from '../helpers';
@@ -99,8 +99,8 @@ describe('deploy freeze store actions', () => {
});
describe('addFreezePeriod', () => {
- it('dispatch correct actions on adding a freeze period', () => {
- testAction(
+ it('dispatch correct actions on adding a freeze period', async () => {
+ await testAction(
actions.addFreezePeriod,
{},
state,
@@ -110,32 +110,33 @@ describe('deploy freeze store actions', () => {
{ type: 'receiveFreezePeriodSuccess' },
{ type: 'fetchFreezePeriods' },
],
- () =>
- expect(Api.createFreezePeriod).toHaveBeenCalledWith(state.projectId, {
- freeze_start: state.freezeStartCron,
- freeze_end: state.freezeEndCron,
- cron_timezone: state.selectedTimezoneIdentifier,
- }),
);
+
+ expect(Api.createFreezePeriod).toHaveBeenCalledWith(state.projectId, {
+ freeze_start: state.freezeStartCron,
+ freeze_end: state.freezeEndCron,
+ cron_timezone: state.selectedTimezoneIdentifier,
+ });
});
- it('should show flash error and set error in state on add failure', () => {
+ it('should show alert and set error in state on add failure', async () => {
Api.createFreezePeriod.mockRejectedValue();
- testAction(
+ await testAction(
actions.addFreezePeriod,
{},
state,
[],
[{ type: 'requestFreezePeriod' }, { type: 'receiveFreezePeriodError' }],
- () => expect(createFlash).toHaveBeenCalled(),
);
+
+ expect(createAlert).toHaveBeenCalled();
});
});
describe('updateFreezePeriod', () => {
- it('dispatch correct actions on updating a freeze period', () => {
- testAction(
+ it('dispatch correct actions on updating a freeze period', async () => {
+ await testAction(
actions.updateFreezePeriod,
{},
state,
@@ -145,33 +146,34 @@ describe('deploy freeze store actions', () => {
{ type: 'receiveFreezePeriodSuccess' },
{ type: 'fetchFreezePeriods' },
],
- () =>
- expect(Api.updateFreezePeriod).toHaveBeenCalledWith(state.projectId, {
- id: state.selectedId,
- freeze_start: state.freezeStartCron,
- freeze_end: state.freezeEndCron,
- cron_timezone: state.selectedTimezoneIdentifier,
- }),
);
+
+ expect(Api.updateFreezePeriod).toHaveBeenCalledWith(state.projectId, {
+ id: state.selectedId,
+ freeze_start: state.freezeStartCron,
+ freeze_end: state.freezeEndCron,
+ cron_timezone: state.selectedTimezoneIdentifier,
+ });
});
- it('should show flash error and set error in state on add failure', () => {
+ it('should show alert and set error in state on add failure', async () => {
Api.updateFreezePeriod.mockRejectedValue();
- testAction(
+ await testAction(
actions.updateFreezePeriod,
{},
state,
[],
[{ type: 'requestFreezePeriod' }, { type: 'receiveFreezePeriodError' }],
- () => expect(createFlash).toHaveBeenCalled(),
);
+
+ expect(createAlert).toHaveBeenCalled();
});
});
describe('fetchFreezePeriods', () => {
it('dispatch correct actions on fetchFreezePeriods', () => {
- testAction(
+ return testAction(
actions.fetchFreezePeriods,
{},
state,
@@ -183,26 +185,26 @@ describe('deploy freeze store actions', () => {
);
});
- it('should show flash error and set error in state on fetch variables failure', () => {
+ it('should show alert and set error in state on fetch variables failure', async () => {
Api.freezePeriods.mockRejectedValue();
- testAction(
+ await testAction(
actions.fetchFreezePeriods,
{},
state,
[{ type: types.REQUEST_FREEZE_PERIODS }],
[],
- () =>
- expect(createFlash).toHaveBeenCalledWith({
- message: 'There was an error fetching the deploy freezes.',
- }),
);
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'There was an error fetching the deploy freezes.',
+ });
});
});
describe('deleteFreezePeriod', () => {
- it('dispatch correct actions on deleting a freeze period', () => {
- testAction(
+ it('dispatch correct actions on deleting a freeze period', async () => {
+ await testAction(
actions.deleteFreezePeriod,
freezePeriodFixture,
state,
@@ -211,20 +213,17 @@ describe('deploy freeze store actions', () => {
{ type: 'RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS', payload: freezePeriodFixture.id },
],
[],
- () =>
- expect(Api.deleteFreezePeriod).toHaveBeenCalledWith(
- state.projectId,
- freezePeriodFixture.id,
- ),
);
+
+ expect(Api.deleteFreezePeriod).toHaveBeenCalledWith(state.projectId, freezePeriodFixture.id);
});
- it('should show flash error and set error in state on delete failure', () => {
+ it('should show alert and set error in state on delete failure', async () => {
jest.spyOn(logger, 'logError').mockImplementation();
const error = new Error();
Api.deleteFreezePeriod.mockRejectedValue(error);
- testAction(
+ await testAction(
actions.deleteFreezePeriod,
freezePeriodFixture,
state,
@@ -233,12 +232,11 @@ describe('deploy freeze store actions', () => {
{ type: 'RECEIVE_DELETE_FREEZE_PERIOD_ERROR', payload: freezePeriodFixture.id },
],
[],
- () => {
- expect(createFlash).toHaveBeenCalled();
-
- expect(logger.logError).toHaveBeenCalledWith('Unable to delete deploy freeze', error);
- },
);
+
+ expect(createAlert).toHaveBeenCalled();
+
+ expect(logger.logError).toHaveBeenCalledWith('Unable to delete deploy freeze', error);
});
});
});
diff --git a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
index 990f18d64c1..0bf69acd251 100644
--- a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
+++ b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
@@ -6,9 +6,21 @@ import axios from '~/lib/utils/axios_utils';
import { TEST_HOST } from 'helpers/test_constants';
import NewDeployToken from '~/deploy_tokens/components/new_deploy_token.vue';
import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert, VARIANT_INFO } from '~/flash';
const createNewTokenPath = `${TEST_HOST}/create`;
const deployTokensHelpUrl = `${TEST_HOST}/help`;
+
+jest.mock('~/flash', () => {
+ const original = jest.requireActual('~/flash');
+
+ return {
+ __esModule: true,
+ ...original,
+ createAlert: jest.fn(),
+ };
+});
+
describe('New Deploy Token', () => {
let wrapper;
@@ -69,9 +81,69 @@ describe('New Deploy Token', () => {
expect(tokenUsername.props('value')).toBe('test token username');
expect(tokenValue.props('value')).toBe('test token');
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variant: VARIANT_INFO,
+ }),
+ );
});
}
+ it('should flash error message if token creation fails', async () => {
+ const mockAxios = new MockAdapter(axios);
+
+ const date = new Date();
+ const formInputs = wrapper.findAllComponents(GlFormInput);
+ const name = formInputs.at(0);
+ const username = formInputs.at(2);
+ name.vm.$emit('input', 'test name');
+ username.vm.$emit('input', 'test username');
+
+ const datepicker = wrapper.findAllComponents(GlDatepicker).at(0);
+ datepicker.vm.$emit('input', date);
+
+ const [
+ readRepo,
+ readRegistry,
+ writeRegistry,
+ readPackageRegistry,
+ writePackageRegistry,
+ ] = wrapper.findAllComponents(GlFormCheckbox).wrappers;
+ readRepo.vm.$emit('input', true);
+ readRegistry.vm.$emit('input', true);
+ writeRegistry.vm.$emit('input', true);
+ readPackageRegistry.vm.$emit('input', true);
+ writePackageRegistry.vm.$emit('input', true);
+
+ const expectedErrorMessage = 'Server error while creating a token';
+
+ mockAxios
+ .onPost(createNewTokenPath, {
+ deploy_token: {
+ name: 'test name',
+ expires_at: date.toISOString(),
+ username: 'test username',
+ read_repository: true,
+ read_registry: true,
+ write_registry: true,
+ read_package_registry: true,
+ write_package_registry: true,
+ },
+ })
+ .replyOnce(500, { message: expectedErrorMessage });
+
+ wrapper.findAllComponents(GlButton).at(0).vm.$emit('click');
+
+ await waitForPromises().then(() => nextTick());
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: expectedErrorMessage,
+ }),
+ );
+ });
+
it('should make a request to create a token on submit', () => {
const mockAxios = new MockAdapter(axios);
diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js
index b3afcefe1ed..ac26873b692 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -3,7 +3,7 @@ import { nextTick } from 'vue';
import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
import createDesignTodoMutation from '~/design_management/graphql/mutations/create_design_todo.mutation.graphql';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
-import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
+import TodoButton from '~/sidebar/components/todo_toggle/todo_button.vue';
import mockDesign from '../mock_data/design';
const mockDesignWithPendingTodos = {
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
index 2b706d21f51..1acbf14db88 100644
--- a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
+++ b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
@@ -1,163 +1,229 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management design version dropdown component renders design version dropdown button 1`] = `
-<gl-dropdown-stub
+<gl-base-dropdown-stub
+ ariahaspopup="listbox"
category="primary"
- clearalltext="Clear all"
- clearalltextclass="gl-px-5"
- headertext=""
- hideheaderborder="true"
- highlighteditemstitle="Selected"
- highlighteditemstitleclass="gl-px-5"
+ icon=""
issueiid=""
projectpath=""
size="small"
- text="Showing latest version"
+ toggleid="dropdown-toggle-btn-2"
+ toggletext="Showing latest version"
variant="default"
>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckcentered="true"
- ischecked="true"
- ischeckitem="true"
- secondarytext=""
+ <!---->
+
+ <!---->
+
+ <ul
+ aria-labelledby="dropdown-toggle-btn-2"
+ class="gl-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0"
+ id="listbox"
+ role="listbox"
+ tabindex="-1"
>
- <strong>
- Version
- 2
- (latest)
- </strong>
-
- <div
- class="gl-text-gray-600 gl-mt-1"
+ <gl-listbox-item-stub
+ ischeckcentered="true"
>
- <div>
- Adminstrator
- </div>
-
- <time-ago-stub
- class="text-1"
- cssclass=""
- time="2021-08-09T06:05:00Z"
- tooltipplacement="bottom"
- />
- </div>
- </gl-dropdown-item-stub>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckcentered="true"
- ischeckitem="true"
- secondarytext=""
- >
- <strong>
- Version
- 1
-
- </strong>
-
- <div
- class="gl-text-gray-600 gl-mt-1"
+ <span
+ class="gl-display-flex gl-align-items-center"
+ >
+ <div
+ class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s32 gl-avatar-identicon-bg1"
+ >
+
+
+
+ </div>
+
+ <span
+ class="gl-display-flex gl-flex-direction-column"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Version 2 (latest)
+ </span>
+
+ <span
+ class="gl-text-gray-600 gl-mt-1"
+ >
+ <span
+ class="gl-display-block"
+ >
+ Adminstrator
+ </span>
+
+ <time-ago-stub
+ class="text-1"
+ cssclass=""
+ time="2021-08-09T06:05:00Z"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </span>
+ </span>
+ </gl-listbox-item-stub>
+ <gl-listbox-item-stub
+ ischeckcentered="true"
>
- <div>
- Adminstrator
- </div>
-
- <time-ago-stub
- class="text-1"
- cssclass=""
- time="2021-08-09T06:05:00Z"
- tooltipplacement="bottom"
- />
- </div>
- </gl-dropdown-item-stub>
-</gl-dropdown-stub>
+ <span
+ class="gl-display-flex gl-align-items-center"
+ >
+ <div
+ class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s32 gl-avatar-identicon-bg1"
+ >
+
+
+
+ </div>
+
+ <span
+ class="gl-display-flex gl-flex-direction-column"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Version 1
+ </span>
+
+ <span
+ class="gl-text-gray-600 gl-mt-1"
+ >
+ <span
+ class="gl-display-block"
+ >
+ Adminstrator
+ </span>
+
+ <time-ago-stub
+ class="text-1"
+ cssclass=""
+ time="2021-08-09T06:05:00Z"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </span>
+ </span>
+ </gl-listbox-item-stub>
+ </ul>
+
+ <!---->
+
+</gl-base-dropdown-stub>
`;
exports[`Design management design version dropdown component renders design version list 1`] = `
-<gl-dropdown-stub
+<gl-base-dropdown-stub
+ ariahaspopup="listbox"
category="primary"
- clearalltext="Clear all"
- clearalltextclass="gl-px-5"
- headertext=""
- hideheaderborder="true"
- highlighteditemstitle="Selected"
- highlighteditemstitleclass="gl-px-5"
+ icon=""
issueiid=""
projectpath=""
size="small"
- text="Showing latest version"
+ toggleid="dropdown-toggle-btn-4"
+ toggletext="Showing latest version"
variant="default"
>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckcentered="true"
- ischecked="true"
- ischeckitem="true"
- secondarytext=""
+ <!---->
+
+ <!---->
+
+ <ul
+ aria-labelledby="dropdown-toggle-btn-4"
+ class="gl-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0"
+ id="listbox"
+ role="listbox"
+ tabindex="-1"
>
- <strong>
- Version
- 2
- (latest)
- </strong>
-
- <div
- class="gl-text-gray-600 gl-mt-1"
+ <gl-listbox-item-stub
+ ischeckcentered="true"
>
- <div>
- Adminstrator
- </div>
-
- <time-ago-stub
- class="text-1"
- cssclass=""
- time="2021-08-09T06:05:00Z"
- tooltipplacement="bottom"
- />
- </div>
- </gl-dropdown-item-stub>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckcentered="true"
- ischeckitem="true"
- secondarytext=""
- >
- <strong>
- Version
- 1
-
- </strong>
-
- <div
- class="gl-text-gray-600 gl-mt-1"
+ <span
+ class="gl-display-flex gl-align-items-center"
+ >
+ <div
+ class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s32 gl-avatar-identicon-bg1"
+ >
+
+
+
+ </div>
+
+ <span
+ class="gl-display-flex gl-flex-direction-column"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Version 2 (latest)
+ </span>
+
+ <span
+ class="gl-text-gray-600 gl-mt-1"
+ >
+ <span
+ class="gl-display-block"
+ >
+ Adminstrator
+ </span>
+
+ <time-ago-stub
+ class="text-1"
+ cssclass=""
+ time="2021-08-09T06:05:00Z"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </span>
+ </span>
+ </gl-listbox-item-stub>
+ <gl-listbox-item-stub
+ ischeckcentered="true"
>
- <div>
- Adminstrator
- </div>
-
- <time-ago-stub
- class="text-1"
- cssclass=""
- time="2021-08-09T06:05:00Z"
- tooltipplacement="bottom"
- />
- </div>
- </gl-dropdown-item-stub>
-</gl-dropdown-stub>
+ <span
+ class="gl-display-flex gl-align-items-center"
+ >
+ <div
+ class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s32 gl-avatar-identicon-bg1"
+ >
+
+
+
+ </div>
+
+ <span
+ class="gl-display-flex gl-flex-direction-column"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Version 1
+ </span>
+
+ <span
+ class="gl-text-gray-600 gl-mt-1"
+ >
+ <span
+ class="gl-display-block"
+ >
+ Adminstrator
+ </span>
+
+ <time-ago-stub
+ class="text-1"
+ cssclass=""
+ time="2021-08-09T06:05:00Z"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </span>
+ </span>
+ </gl-listbox-item-stub>
+ </ul>
+
+ <!---->
+
+</gl-base-dropdown-stub>
`;
diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
index 7c26ab9739b..1e9f286a0ec 100644
--- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import { GlAvatar, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import DesignVersionDropdown from '~/design_management/components/upload/design_version_dropdown.vue';
@@ -32,7 +32,7 @@ describe('Design management design version dropdown component', () => {
mocks: {
$route,
},
- stubs: { GlSprintf },
+ stubs: { GlAvatar, GlCollapsibleListbox },
});
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
@@ -46,7 +46,9 @@ describe('Design management design version dropdown component', () => {
wrapper.destroy();
});
- const findVersionLink = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
+ const findVersionLink = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
it('renders design version dropdown button', async () => {
createComponent();
@@ -76,35 +78,36 @@ describe('Design management design version dropdown component', () => {
createComponent();
await nextTick();
- expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version');
+
+ expect(findListbox().props('toggleText')).toBe('Showing latest version');
});
it('displays latest version text when only 1 version is present', async () => {
createComponent({ maxVersions: 1 });
await nextTick();
- expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version');
+ expect(findListbox().props('toggleText')).toBe('Showing latest version');
});
it('displays version text when the current version is not the latest', async () => {
createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) });
await nextTick();
- expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe(`Showing version #1`);
+ expect(findListbox().props('toggleText')).toBe(`Showing version #1`);
});
it('displays latest version text when the current version is the latest', async () => {
createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) });
await nextTick();
- expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version');
+ expect(findListbox().props('toggleText')).toBe('Showing latest version');
});
it('should have the same length as apollo query', async () => {
createComponent();
await nextTick();
- expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
+ expect(findAllListboxItems()).toHaveLength(wrapper.vm.allVersions.length);
});
it('should render TimeAgo', async () => {
diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js
index b5dce4fc924..7bd9afab648 100644
--- a/spec/frontend/diffs/components/diff_code_quality_spec.js
+++ b/spec/frontend/diffs/components/diff_code_quality_spec.js
@@ -1,12 +1,14 @@
import { GlIcon } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue';
-import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/reports/codequality_report/constants';
+import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
+import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
import { multipleFindingsArr } from '../mock_data/diff_code_quality';
let wrapper;
const findIcon = () => wrapper.findComponent(GlIcon);
+const findHeading = () => wrapper.findByTestId(`diff-codequality-findings-heading`);
describe('DiffCodeQuality', () => {
afterEach(() => {
@@ -30,14 +32,17 @@ describe('DiffCodeQuality', () => {
expect(wrapper.emitted('hideCodeQualityFindings').length).toBe(1);
});
- it('renders correct amount of list items for codequality array and their description', async () => {
+ it('renders heading and correct amount of list items for codequality array and their description', async () => {
wrapper = createWrapper(multipleFindingsArr);
- const listItems = wrapper.findAll('li');
+ expect(findHeading().text()).toEqual(NEW_CODE_QUALITY_FINDINGS);
- expect(wrapper.findAll('li').length).toBe(3);
+ const listItems = wrapper.findAll('li');
+ expect(wrapper.findAll('li').length).toBe(5);
listItems.wrappers.map((e, i) => {
- return expect(e.text()).toEqual(multipleFindingsArr[i].description);
+ return expect(e.text()).toContain(
+ `${multipleFindingsArr[i].severity} - ${multipleFindingsArr[i].description}`,
+ );
});
});
diff --git a/spec/frontend/diffs/components/diff_discussion_reply_spec.js b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
index 5ccd2002462..bf4a1a1c1f7 100644
--- a/spec/frontend/diffs/components/diff_discussion_reply_spec.js
+++ b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
@@ -1,10 +1,12 @@
import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import DiffDiscussionReply from '~/diffs/components/diff_discussion_reply.vue';
-import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
+import { START_THREAD } from '~/diffs/i18n';
+
Vue.use(Vuex);
describe('DiffDiscussionReply', () => {
@@ -58,14 +60,42 @@ describe('DiffDiscussionReply', () => {
expect(wrapper.find('#test-form').exists()).toBe(true);
});
- it('should render a reply placeholder if there is no form', () => {
+ it('should render a reply placeholder button if there is no form', () => {
createComponent({
renderReplyPlaceholder: true,
hasForm: false,
});
- expect(wrapper.findComponent(ReplyPlaceholder).exists()).toBe(true);
+ expect(wrapper.findComponent(GlButton).text()).toBe(START_THREAD);
});
+
+ it.each`
+ userCanReply | hasForm | renderReplyPlaceholder | showButton
+ ${false} | ${false} | ${false} | ${false}
+ ${true} | ${false} | ${false} | ${false}
+ ${true} | ${true} | ${false} | ${false}
+ ${true} | ${true} | ${true} | ${false}
+ ${true} | ${false} | ${true} | ${true}
+ ${false} | ${false} | ${true} | ${false}
+ `(
+ 'reply button existence is `$showButton` when userCanReply is `$userCanReply`, hasForm is `$hasForm` and renderReplyPlaceholder is `$renderReplyPlaceholder`',
+ ({ userCanReply, hasForm, renderReplyPlaceholder, showButton }) => {
+ getters = {
+ userCanReply: () => userCanReply,
+ };
+
+ store = new Vuex.Store({
+ getters,
+ });
+
+ createComponent({
+ renderReplyPlaceholder,
+ hasForm,
+ });
+
+ expect(wrapper.findComponent(GlButton).exists()).toBe(showButton);
+ },
+ );
});
it('renders a signed out widget when user is not logged in', () => {
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index e9a0e0745fd..5092ae6ab6e 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -5,9 +5,10 @@ import { createStore } from '~/mr_notes/stores';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import '~/behaviors/markdown/render_gfm';
import discussionsMockData from '../mock_data/diff_discussions';
+jest.mock('~/behaviors/markdown/render_gfm');
+
describe('DiffDiscussions', () => {
let store;
let wrapper;
diff --git a/spec/frontend/diffs/mock_data/diff_code_quality.js b/spec/frontend/diffs/mock_data/diff_code_quality.js
index befab3b676b..7558592f6a4 100644
--- a/spec/frontend/diffs/mock_data/diff_code_quality.js
+++ b/spec/frontend/diffs/mock_data/diff_code_quality.js
@@ -1,25 +1,39 @@
export const multipleFindingsArr = [
{
severity: 'minor',
- description: 'Unexpected Debugger Statement.',
+ description: 'mocked minor Issue',
line: 2,
},
{
severity: 'major',
- description:
- 'Function `aVeryLongFunction` has 52 lines of code (exceeds 25 allowed). Consider refactoring.',
+ description: 'mocked major Issue',
line: 3,
},
{
- severity: 'minor',
- description: 'Arrow function has too many statements (52). Maximum allowed is 30.',
+ severity: 'info',
+ description: 'mocked info Issue',
+ line: 3,
+ },
+ {
+ severity: 'critical',
+ description: 'mocked critical Issue',
+ line: 3,
+ },
+ {
+ severity: 'blocker',
+ description: 'mocked blocker Issue',
line: 3,
},
];
-export const multipleFindings = {
+export const fiveFindings = {
+ filePath: 'index.js',
+ codequality: multipleFindingsArr.slice(0, 5),
+};
+
+export const threeFindings = {
filePath: 'index.js',
- codequality: multipleFindingsArr,
+ codequality: multipleFindingsArr.slice(0, 3),
};
export const singularFinding = {
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 87366cdbfc5..9e0ffbf757f 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -606,6 +606,50 @@ describe('DiffsStoreActions', () => {
params: { commit_id: '123', w: '0' },
});
});
+
+ describe('version parameters', () => {
+ const diffId = '4';
+ const startSha = 'abc';
+ const pathRoot = 'a/a/-/merge_requests/1';
+ let file;
+ let getters;
+
+ beforeAll(() => {
+ file = { load_collapsed_diff_url: '/load/collapsed/diff/url' };
+ getters = {};
+ });
+
+ beforeEach(() => {
+ jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} }));
+ });
+
+ it('fetches the data when there is no mergeRequestDiff', () => {
+ diffActions.loadCollapsedDiff({ commit() {}, getters, state }, file);
+
+ expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
+ params: expect.any(Object),
+ });
+ });
+
+ it.each`
+ desc | versionPath | start_sha | diff_id
+ ${'no additional version information'} | ${`${pathRoot}?search=terms`} | ${undefined} | ${undefined}
+ ${'the diff_id'} | ${`${pathRoot}?diff_id=${diffId}`} | ${undefined} | ${diffId}
+ ${'the start_sha'} | ${`${pathRoot}?start_sha=${startSha}`} | ${startSha} | ${undefined}
+ ${'all available version information'} | ${`${pathRoot}?diff_id=${diffId}&start_sha=${startSha}`} | ${startSha} | ${diffId}
+ `('fetches the data and includes $desc', ({ versionPath, start_sha, diff_id }) => {
+ jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} }));
+
+ diffActions.loadCollapsedDiff(
+ { commit() {}, getters, state: { mergeRequestDiff: { version_path: versionPath } } },
+ file,
+ );
+
+ expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
+ params: expect.objectContaining({ start_sha, diff_id }),
+ });
+ });
+ });
});
describe('toggleFileDiscussions', () => {
diff --git a/spec/frontend/diffs/utils/merge_request_spec.js b/spec/frontend/diffs/utils/merge_request_spec.js
index 8c7b1e1f2a5..c070e8c004d 100644
--- a/spec/frontend/diffs/utils/merge_request_spec.js
+++ b/spec/frontend/diffs/utils/merge_request_spec.js
@@ -2,30 +2,64 @@ import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
import { diffMetadata } from '../mock_data/diff_metadata';
describe('Merge Request utilities', () => {
- const derivedMrInfo = {
+ const derivedBaseInfo = {
mrPath: '/gitlab-org/gitlab-test/-/merge_requests/4',
userOrGroup: 'gitlab-org',
project: 'gitlab-test',
id: '4',
};
+ const derivedVersionInfo = {
+ diffId: '4',
+ startSha: 'eb227b3e214624708c474bdab7bde7afc17cefcc',
+ };
+ const noVersion = {
+ diffId: undefined,
+ startSha: undefined,
+ };
const unparseableEndpoint = {
mrPath: undefined,
userOrGroup: undefined,
project: undefined,
id: undefined,
+ ...noVersion,
};
describe('getDerivedMergeRequestInformation', () => {
- const endpoint = `${diffMetadata.latest_version_path}.json?searchParam=irrelevant`;
+ let endpoint = `${diffMetadata.latest_version_path}.json?searchParam=irrelevant`;
it.each`
argument | response
- ${{ endpoint }} | ${derivedMrInfo}
+ ${{ endpoint }} | ${{ ...derivedBaseInfo, ...noVersion }}
${{}} | ${unparseableEndpoint}
${{ endpoint: undefined }} | ${unparseableEndpoint}
${{ endpoint: null }} | ${unparseableEndpoint}
`('generates the correct derived results based on $argument', ({ argument, response }) => {
expect(getDerivedMergeRequestInformation(argument)).toStrictEqual(response);
});
+
+ describe('version information', () => {
+ const bare = diffMetadata.latest_version_path;
+ endpoint = diffMetadata.merge_request_diffs[0].compare_path;
+
+ it('still gets the correct derived information', () => {
+ expect(getDerivedMergeRequestInformation({ endpoint })).toMatchObject(derivedBaseInfo);
+ });
+
+ it.each`
+ url | versionPart
+ ${endpoint} | ${derivedVersionInfo}
+ ${`${bare}?diff_id=${derivedVersionInfo.diffId}`} | ${{ ...derivedVersionInfo, startSha: undefined }}
+ ${`${bare}?start_sha=${derivedVersionInfo.startSha}`} | ${{ ...derivedVersionInfo, diffId: undefined }}
+ `(
+ 'generates the correct derived version information based on $url',
+ ({ url, versionPart }) => {
+ expect(getDerivedMergeRequestInformation({ endpoint: url })).toMatchObject(versionPart);
+ },
+ );
+
+ it('extracts nothing if there is no available version-like information in the URL', () => {
+ expect(getDerivedMergeRequestInformation({ endpoint: bare })).toMatchObject(noVersion);
+ });
+ });
});
});
diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
index 1475d451ab3..ff377494312 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
@@ -15,6 +15,9 @@ describe('Source Editor Toolbar button', () => {
propsData: {
...props,
},
+ stubs: {
+ GlButton,
+ },
});
};
@@ -52,9 +55,69 @@ describe('Source Editor Toolbar button', () => {
const btn = findButton();
expect(btn.props()).toMatchObject(customProps);
});
+
+ describe('CSS class', () => {
+ let blueprintClasses;
+
+ beforeEach(() => {
+ createComponent();
+ blueprintClasses = findButton().element.classList;
+ });
+
+ it.each`
+ cssClass | expectedExtraClasses
+ ${undefined} | ${['']}
+ ${''} | ${['']}
+ ${'foo'} | ${['foo']}
+ ${'foo bar'} | ${['foo', 'bar']}
+ `(
+ 'does set CSS class correctly when `class` is "$cssClass"',
+ ({ cssClass, expectedExtraClasses }) => {
+ createComponent({
+ button: {
+ ...defaultBtn,
+ class: cssClass,
+ },
+ });
+ const btn = findButton().element;
+ expectedExtraClasses.forEach((c) => {
+ if (c) {
+ expect(btn.classList.contains(c)).toBe(true);
+ } else {
+ expect(btn.classList).toEqual(blueprintClasses);
+ }
+ });
+ },
+ );
+ });
+ });
+
+ describe('data attributes', () => {
+ it.each`
+ description | data | expectedDataset
+ ${'does not set any attribute'} | ${undefined} | ${{}}
+ ${'does not set any attribute'} | ${[]} | ${{}}
+ ${'does not set any attribute'} | ${['foo']} | ${{}}
+ ${'does not set any attribute'} | ${'bar'} | ${{}}
+ ${'does set single attribute correctly'} | ${{ qaSelector: 'foo' }} | ${{ qaSelector: 'foo' }}
+ ${'does set multiple attributes correctly'} | ${{ qaSelector: 'foo', youCanSeeMe: true }} | ${{ qaSelector: 'foo', youCanSeeMe: 'true' }}
+ `('$description when data="$data"', ({ data, expectedDataset }) => {
+ createComponent({
+ button: {
+ data,
+ },
+ });
+ expect(findButton().element.dataset).toEqual(expect.objectContaining(expectedDataset));
+ });
});
describe('click handler', () => {
+ let clickEvent;
+
+ beforeEach(() => {
+ clickEvent = new Event('click');
+ });
+
it('fires the click handler on the button when available', async () => {
const spy = jest.fn();
createComponent({
@@ -63,20 +126,20 @@ describe('Source Editor Toolbar button', () => {
},
});
expect(spy).not.toHaveBeenCalled();
- findButton().vm.$emit('click');
+ findButton().vm.$emit('click', clickEvent);
await nextTick();
- expect(spy).toHaveBeenCalled();
+ expect(spy).toHaveBeenCalledWith(clickEvent);
});
- it('emits the "click" event', async () => {
+ it('emits the "click" event, passing the event itself', async () => {
createComponent();
jest.spyOn(wrapper.vm, '$emit');
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
- findButton().vm.$emit('click');
+ findButton().vm.$emit('click', clickEvent);
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', clickEvent);
});
});
});
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
index 32126a5fd9a..c822a0bfeaf 100644
--- a/spec/frontend/editor/schema/ci/ci_schema_spec.js
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -30,6 +30,9 @@ import RulesYaml from './yaml_tests/positive_tests/rules.yml';
import ProjectPathYaml from './yaml_tests/positive_tests/project_path.yml';
import VariablesYaml from './yaml_tests/positive_tests/variables.yml';
import JobWhenYaml from './yaml_tests/positive_tests/job_when.yml';
+import IdTokensYaml from './yaml_tests/positive_tests/id_tokens.yml';
+import HooksYaml from './yaml_tests/positive_tests/hooks.yml';
+import SecretsYaml from './yaml_tests/positive_tests/secrets.yml';
// YAML NEGATIVE TEST
import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
@@ -43,8 +46,12 @@ import ProjectPathIncludeNoSlashYaml from './yaml_tests/negative_tests/project_p
import ProjectPathIncludeTailSlashYaml from './yaml_tests/negative_tests/project_path/include/tailing_slash.yml';
import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml';
import TriggerNegative from './yaml_tests/negative_tests/trigger.yml';
+import VariablesInvalidOptionsYaml from './yaml_tests/negative_tests/variables/invalid_options.yml';
import VariablesInvalidSyntaxDescYaml from './yaml_tests/negative_tests/variables/invalid_syntax_desc.yml';
import VariablesWrongSyntaxUsageExpand from './yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml';
+import IdTokensNegativeYaml from './yaml_tests/negative_tests/id_tokens.yml';
+import HooksNegative from './yaml_tests/negative_tests/hooks.yml';
+import SecretsNegativeYaml from './yaml_tests/negative_tests/secrets.yml';
const ajv = new Ajv({
strictTypes: false,
@@ -77,9 +84,12 @@ describe('positive tests', () => {
FilterYaml,
IncludeYaml,
JobWhenYaml,
+ HooksYaml,
RulesYaml,
VariablesYaml,
ProjectPathYaml,
+ IdTokensYaml,
+ SecretsYaml,
}),
)('schema validates %s', (_, input) => {
// We construct a new "JSON" from each main key that is inside a
@@ -103,9 +113,11 @@ describe('negative tests', () => {
// YAML
ArtifactsNegativeYaml,
CacheKeyNeative,
+ IdTokensNegativeYaml,
IncludeNegativeYaml,
JobWhenNegativeYaml,
RulesNegativeYaml,
+ VariablesInvalidOptionsYaml,
VariablesInvalidSyntaxDescYaml,
VariablesWrongSyntaxUsageExpand,
ProjectPathIncludeEmptyYaml,
@@ -113,7 +125,9 @@ describe('negative tests', () => {
ProjectPathIncludeLeadSlashYaml,
ProjectPathIncludeNoSlashYaml,
ProjectPathIncludeTailSlashYaml,
+ SecretsNegativeYaml,
TriggerNegative,
+ HooksNegative,
}),
)('schema validates %s', (_, input) => {
// We construct a new "JSON" from each main key that is inside a
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
index f5670376efc..29f4a0cd76d 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
@@ -16,3 +16,16 @@ cyclonedx not an array or string:
paths:
- foo
- bar
+
+# invalid artifacts:when
+artifacts-when-unknown:
+ artifacts:
+ when: unknown
+
+artifacts-when-array:
+ artifacts:
+ when: [always]
+
+artifacts-when-boolean:
+ artifacts:
+ when: true
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
index 3979c9ae2ac..9baed2a7922 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
@@ -51,12 +51,39 @@ cache-untracked-string:
cache:
untracked: 'true'
-when_integer:
+# invalid cache:when
+cache-when-integer:
script: echo "This job uses a cache."
cache:
when: 0
-when_not_reserved_keyword:
+cache-when-array:
+ script: echo "This job uses a cache."
+ cache:
+ when: [always]
+
+cache-when-boolean:
+ script: echo "This job uses a cache."
+ cache:
+ when: true
+
+cache-when-never:
script: echo "This job uses a cache."
cache:
when: 'never'
+
+# invalid cache:policy
+cache-policy-array:
+ script: echo "This job uses a cache."
+ cache:
+ policy: [push]
+
+cache-policy-boolean:
+ script: echo "This job uses a cache."
+ cache:
+ policy: true
+
+cache-when-unknown:
+ script: echo "This job uses a cache."
+ cache:
+ policy: unknown
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/hooks.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/hooks.yml
new file mode 100644
index 00000000000..e3366b0b6d3
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/hooks.yml
@@ -0,0 +1,10 @@
+job1:
+ hooks:
+ invalid_script:
+ - echo 'hello job1 invalid_script'
+ script: echo 'hello job1 script'
+
+job2:
+ hooks:
+ pre_get_sources_script: true
+ script: echo 'hello job1 script'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/id_tokens.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/id_tokens.yml
new file mode 100644
index 00000000000..aff2611f16c
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/id_tokens.yml
@@ -0,0 +1,11 @@
+id_token_with_wrong_aud_type:
+ id_tokens:
+ INVALID_ID_TOKEN:
+ aud:
+ invalid_prop: invalid
+
+id_token_with_extra_properties:
+ id_tokens:
+ INVALID_ID_TOKEN:
+ aud: 'https://gitlab.com'
+ sub: 'not a valid property'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml
new file mode 100644
index 00000000000..14ba930b394
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml
@@ -0,0 +1,39 @@
+job_with_secrets_without_vault:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ token: $TEST_TOKEN
+
+job_with_secrets_with_extra_properties:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault: test/db/password
+ extra_prop: TEST
+
+job_with_secrets_with_invalid_vault_property:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault:
+ invalid: TEST
+
+job_with_secrets_with_missing_required_vault_property:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault:
+ path: gitlab
+
+job_with_secrets_with_missing_required_engine_property:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault:
+ engine:
+ path: kv
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_options.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_options.yml
new file mode 100644
index 00000000000..aac4c4e456d
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_options.yml
@@ -0,0 +1,4 @@
+variables:
+ INVALID_OPTIONS:
+ value: "staging"
+ options: "staging" # must be an array
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
index 20c1fc2c50f..a5c9153ee13 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
@@ -23,3 +23,13 @@ cylonedx mixed list of string paths and globs:
cyclonedx:
- ./foo
- "bar/*.baz"
+
+# valid artifacts:when
+artifacts-when-on-failure:
+ artifacts:
+ when: on_failure
+
+artifacts-no-when:
+ artifacts:
+ paths:
+ - binaries/
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
index 75918cd2a1b..d50b74e1448 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
@@ -122,3 +122,20 @@ cache-untracked-false:
script: test
cache:
untracked: false
+
+# valid cache:policy
+cache-policy-push:
+ script: echo "This job uses a cache."
+ cache:
+ policy: push
+
+cache-policy-pull:
+ script: echo "This job uses a cache."
+ cache:
+ policy: pull
+
+cache-no-policy:
+ script: echo "This job uses a cache."
+ cache:
+ paths:
+ - binaries/
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/hooks.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/hooks.yml
new file mode 100644
index 00000000000..4d45c5528ea
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/hooks.yml
@@ -0,0 +1,10 @@
+default:
+ hooks:
+ pre_get_sources_script:
+ - echo 'hello default pre_get_sources_script'
+
+job1:
+ hooks:
+ pre_get_sources_script:
+ - echo 'hello job1 pre_get_sources_script'
+ script: echo 'hello job1 script'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/id_tokens.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/id_tokens.yml
new file mode 100644
index 00000000000..169b09ee56f
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/id_tokens.yml
@@ -0,0 +1,11 @@
+valid_id_tokens:
+ script:
+ - echo $ID_TOKEN_1
+ - echo $ID_TOKEN_2
+ id_tokens:
+ ID_TOKEN_1:
+ aud: 'https://gitlab.com'
+ ID_TOKEN_2:
+ aud:
+ - 'https://aws.com'
+ - 'https://google.com'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml
new file mode 100644
index 00000000000..083cb4348ed
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml
@@ -0,0 +1,28 @@
+valid_job_with_secrets:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault: test/db/password
+
+valid_job_with_secrets_and_token:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault: test/db/password
+ token: $TEST_TOKEN
+
+valid_job_with_secrets_with_every_vault_keyword:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault:
+ engine:
+ name: test-engine
+ path: test
+ path: test/db
+ field: password
+ file: true
+ token: $TEST_TOKEN
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml
index 53d020c432f..5c91de9be70 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml
@@ -4,11 +4,18 @@ variables:
FOO:
value: "BAR"
description: "A single value variable"
- DEPLOY_ENVIRONMENT:
+ VAR_WITH_DESCRIPTION:
description: "A multi-value variable"
RAW_VAR:
value: "Hello $FOO"
expand: false
+ VAR_WITH_OPTIONS:
+ value: "staging"
+ options:
+ - "production"
+ - "staging"
+ - "canary"
+ description: "The deployment target. Set to 'production' by default."
rspec:
script: rspec
diff --git a/spec/frontend/editor/source_editor_markdown_ext_spec.js b/spec/frontend/editor/source_editor_markdown_ext_spec.js
index 3e8c287df2f..33e4b4bfc8e 100644
--- a/spec/frontend/editor/source_editor_markdown_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_ext_spec.js
@@ -1,7 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
import { Range, Position } from 'monaco-editor';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { EXTENSION_MARKDOWN_BUTTONS } from '~/editor/constants';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
+import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
import SourceEditor from '~/editor/source_editor';
import axios from '~/lib/utils/axios_utils';
@@ -36,7 +38,7 @@ describe('Markdown Extension for Source Editor', () => {
blobPath: markdownPath,
blobContent: text,
});
- instance.use({ definition: EditorMarkdownExtension });
+ instance.use([{ definition: ToolbarExtension }, { definition: EditorMarkdownExtension }]);
});
afterEach(() => {
@@ -47,6 +49,16 @@ describe('Markdown Extension for Source Editor', () => {
resetHTMLFixture();
});
+ describe('toolbar', () => {
+ it('renders all the buttons', () => {
+ const btns = instance.toolbar.getAllItems();
+ expect(btns).toHaveLength(EXTENSION_MARKDOWN_BUTTONS.length);
+ EXTENSION_MARKDOWN_BUTTONS.forEach((btn, i) => {
+ expect(btns[i].id).toBe(btn.id);
+ });
+ });
+ });
+
describe('getSelectedText', () => {
it('does not fail if there is no selection and returns the empty string', () => {
jest.spyOn(instance, 'getSelection');
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 1c84350bd8e..82e3b50aeb8 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -1,7 +1,7 @@
/* eslint-disable import/no-commonjs, max-classes-per-file */
const path = require('path');
-const JSDOMEnvironment = require('jest-environment-jsdom');
+const { TestEnvironment } = require('jest-environment-jsdom');
const { ErrorWithStack } = require('jest-util');
const {
setGlobalDateToFakeDate,
@@ -11,10 +11,10 @@ const { TEST_HOST } = require('./__helpers__/test_constants');
const ROOT_PATH = path.resolve(__dirname, '../..');
-class CustomEnvironment extends JSDOMEnvironment {
- constructor(config, context) {
+class CustomEnvironment extends TestEnvironment {
+ constructor({ globalConfig, projectConfig }, context) {
// Setup testURL so that window.location is setup properly
- super({ ...config, testURL: TEST_HOST }, context);
+ super({ globalConfig, projectConfig: { ...projectConfig, testURL: TEST_HOST } }, context);
// Fake the `Date` for `jsdom` which fixes things like document.cookie
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39496#note_503084332
@@ -39,8 +39,7 @@ class CustomEnvironment extends JSDOMEnvironment {
},
});
- const { testEnvironmentOptions } = config;
- const { IS_EE } = testEnvironmentOptions;
+ const { IS_EE } = projectConfig.testEnvironmentOptions;
this.global.gon = {
ee: IS_EE,
};
diff --git a/spec/frontend/environments/environment_details_page_spec.js b/spec/frontend/environments/environment_details_page_spec.js
new file mode 100644
index 00000000000..5a02b34250f
--- /dev/null
+++ b/spec/frontend/environments/environment_details_page_spec.js
@@ -0,0 +1,50 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
+import resolvedEnvironmentDetails from 'test_fixtures/graphql/environments/graphql/queries/environment_details.query.graphql.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from '../__helpers__/mock_apollo_helper';
+import waitForPromises from '../__helpers__/wait_for_promises';
+import EnvironmentsDetailPage from '../../../app/assets/javascripts/environments/environment_details/index.vue';
+import getEnvironmentDetails from '../../../app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql';
+
+describe('~/environments/environment_details/page.vue', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const createWrapper = () => {
+ const mockApollo = createMockApollo([
+ [getEnvironmentDetails, jest.fn().mockResolvedValue(resolvedEnvironmentDetails)],
+ ]);
+
+ return mountExtended(EnvironmentsDetailPage, {
+ apolloProvider: mockApollo,
+ propsData: {
+ projectFullPath: resolvedEnvironmentDetails.data.project.fullPath,
+ environmentName: resolvedEnvironmentDetails.data.project.environment.name,
+ },
+ });
+ };
+
+ describe('when fetching data', () => {
+ it('should show a loading indicator', () => {
+ wrapper = createWrapper();
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlTableLite).exists()).not.toBe(true);
+ });
+ });
+
+ describe('when data is fetched', () => {
+ beforeEach(async () => {
+ wrapper = createWrapper();
+ await waitForPromises();
+ });
+
+ it('should render a table when query is loaded', async () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).not.toBe(true);
+ expect(wrapper.findComponent(GlTableLite).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap b/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
new file mode 100644
index 00000000000..401c10338c1
--- /dev/null
+++ b/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
@@ -0,0 +1,127 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`deployment_data_transformation_helper convertToDeploymentTableRow should be converted to proper table row data 1`] = `
+Object {
+ "commit": Object {
+ "author": Object {
+ "avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
+ "path": "http://gdk.test:3000/root",
+ "username": "Administrator",
+ },
+ "commitRef": Object {
+ "name": "main",
+ },
+ "commitUrl": "http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ "shortSha": "0cb48dd5",
+ "tag": false,
+ "title": "Update .gitlab-ci.yml file",
+ },
+ "created": "2022-10-17T07:44:17Z",
+ "deployed": "2022-10-17T07:44:43Z",
+ "id": "31",
+ "job": Object {
+ "label": "deploy-prod (#860)",
+ "webPath": "/gitlab-org/pipelinestest/-/jobs/860",
+ },
+ "status": "success",
+ "triggerer": Object {
+ "avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
+ "id": "gid://gitlab/User/1",
+ "name": "Administrator",
+ "webUrl": "http://gdk.test:3000/root",
+ },
+}
+`;
+
+exports[`deployment_data_transformation_helper convertToDeploymentTableRow should be converted to proper table row data 2`] = `
+Object {
+ "commit": Object {
+ "author": Object {
+ "avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
+ "path": "http://gdk.test:3000/root",
+ "username": "Administrator",
+ },
+ "commitRef": Object {
+ "name": "main",
+ },
+ "commitUrl": "http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ "shortSha": "0cb48dd5",
+ "tag": false,
+ "title": "Update .gitlab-ci.yml file",
+ },
+ "created": "2022-10-17T07:44:17Z",
+ "deployed": "2022-10-17T07:44:43Z",
+ "id": "31",
+ "job": undefined,
+ "status": "success",
+ "triggerer": Object {
+ "avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
+ "id": "gid://gitlab/User/1",
+ "name": "Administrator",
+ "webUrl": "http://gdk.test:3000/root",
+ },
+}
+`;
+
+exports[`deployment_data_transformation_helper convertToDeploymentTableRow should be converted to proper table row data 3`] = `
+Object {
+ "commit": Object {
+ "author": Object {
+ "avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
+ "path": "http://gdk.test:3000/root",
+ "username": "Administrator",
+ },
+ "commitRef": Object {
+ "name": "main",
+ },
+ "commitUrl": "http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ "shortSha": "0cb48dd5",
+ "tag": false,
+ "title": "Update .gitlab-ci.yml file",
+ },
+ "created": "2022-10-17T07:44:17Z",
+ "deployed": "",
+ "id": "31",
+ "job": null,
+ "status": "success",
+ "triggerer": Object {
+ "avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
+ "id": "gid://gitlab/User/1",
+ "name": "Administrator",
+ "webUrl": "http://gdk.test:3000/root",
+ },
+}
+`;
+
+exports[`deployment_data_transformation_helper getAuthorFromCommit should be properly converted 1`] = `
+Object {
+ "avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
+ "path": "http://gdk.test:3000/root",
+ "username": "Administrator",
+}
+`;
+
+exports[`deployment_data_transformation_helper getAuthorFromCommit should be properly converted 2`] = `
+Object {
+ "avatar_url": "https://www.gravatar.com/avatar/91811aee1dec1b2655fa56f894e9e7c9?s=80&d=identicon",
+ "path": "mailto:azubov@gitlab.com",
+ "username": "Andrei Zubov",
+}
+`;
+
+exports[`deployment_data_transformation_helper getCommitFromDeploymentNode should get correclty formatted commit object 1`] = `
+Object {
+ "author": Object {
+ "avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
+ "path": "http://gdk.test:3000/root",
+ "username": "Administrator",
+ },
+ "commitRef": Object {
+ "name": "main",
+ },
+ "commitUrl": "http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ "shortSha": "0cb48dd5",
+ "tag": false,
+ "title": "Update .gitlab-ci.yml file",
+}
+`;
diff --git a/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js b/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js
new file mode 100644
index 00000000000..8bb87c0a208
--- /dev/null
+++ b/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js
@@ -0,0 +1,96 @@
+import {
+ getAuthorFromCommit,
+ getCommitFromDeploymentNode,
+ convertToDeploymentTableRow,
+} from '~/environments/helpers/deployment_data_transformation_helper';
+
+describe('deployment_data_transformation_helper', () => {
+ const commitWithAuthor = {
+ id: 'gid://gitlab/CommitPresenter/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74',
+ shortId: '0cb48dd5',
+ message: 'Update .gitlab-ci.yml file',
+ webUrl:
+ 'http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74',
+ authorGravatar:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ authorName: 'Administrator',
+ authorEmail: 'admin@example.com',
+ author: {
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png',
+ webUrl: 'http://gdk.test:3000/root',
+ },
+ };
+
+ const commitWithourAuthor = {
+ id: 'gid://gitlab/CommitPresenter/02274a949a88c9aef68a29685d99bd9a661a7f9b',
+ shortId: '02274a94',
+ message: 'Commit message',
+ webUrl:
+ 'http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/02274a949a88c9aef68a29685d99bd9a661a7f9b',
+ authorGravatar:
+ 'https://www.gravatar.com/avatar/91811aee1dec1b2655fa56f894e9e7c9?s=80&d=identicon',
+ authorName: 'Andrei Zubov',
+ authorEmail: 'azubov@gitlab.com',
+ author: null,
+ };
+
+ const deploymentNode = {
+ id: 'gid://gitlab/Deployment/76',
+ iid: '31',
+ status: 'SUCCESS',
+ createdAt: '2022-10-17T07:44:17Z',
+ ref: 'main',
+ tag: false,
+ job: {
+ name: 'deploy-prod',
+ refName: 'main',
+ id: 'gid://gitlab/Ci::Build/860',
+ webPath: '/gitlab-org/pipelinestest/-/jobs/860',
+ },
+ commit: commitWithAuthor,
+ triggerer: {
+ id: 'gid://gitlab/User/1',
+ webUrl: 'http://gdk.test:3000/root',
+ name: 'Administrator',
+ avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png',
+ },
+ finishedAt: '2022-10-17T07:44:43Z',
+ };
+
+ const deploymentNodeWithNoJob = {
+ ...deploymentNode,
+ job: null,
+ finishedAt: null,
+ };
+
+ describe('getAuthorFromCommit', () => {
+ it.each([commitWithAuthor, commitWithourAuthor])('should be properly converted', (commit) => {
+ expect(getAuthorFromCommit(commit)).toMatchSnapshot();
+ });
+ });
+
+ describe('getCommitFromDeploymentNode', () => {
+ it('should throw an error when commit field is missing', () => {
+ const emptyDeploymentNode = {};
+
+ expect(() => getCommitFromDeploymentNode(emptyDeploymentNode)).toThrow();
+ });
+
+ it('should get correclty formatted commit object', () => {
+ expect(getCommitFromDeploymentNode(deploymentNode)).toMatchSnapshot();
+ });
+ });
+
+ describe('convertToDeploymentTableRow', () => {
+ const deploymentNodeWithEmptyJob = { ...deploymentNode, job: undefined };
+
+ it.each([deploymentNode, deploymentNodeWithEmptyJob, deploymentNodeWithNoJob])(
+ 'should be converted to proper table row data',
+ (node) => {
+ expect(convertToDeploymentTableRow(node)).toMatchSnapshot();
+ },
+ );
+ });
+});
diff --git a/spec/frontend/feature_flags/components/feature_flags_table_spec.js b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
index 47f12f70056..f23bca54b55 100644
--- a/spec/frontend/feature_flags/components/feature_flags_table_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
@@ -1,6 +1,6 @@
-import { GlToggle, GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlToggle } from '@gitlab/ui';
import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import { mockTracking } from 'helpers/tracking_helper';
import FeatureFlagsTable from '~/feature_flags/components/feature_flags_table.vue';
@@ -52,10 +52,10 @@ const getDefaultProps = () => ({
describe('Feature flag table', () => {
let wrapper;
let props;
- let badges;
+ let labels;
const createWrapper = (propsData, opts = {}) => {
- wrapper = shallowMount(FeatureFlagsTable, {
+ wrapper = mountExtended(FeatureFlagsTable, {
propsData,
provide: {
csrfToken: 'fakeToken',
@@ -70,18 +70,13 @@ describe('Feature flag table', () => {
provide: { csrfToken: 'fakeToken' },
});
- badges = wrapper.findAll('[data-testid="strategy-badge"]');
+ labels = wrapper.findAllByTestId('strategy-label');
});
beforeEach(() => {
props = getDefaultProps();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with an active scope and a standard rollout strategy', () => {
beforeEach(() => {
createWrapper(props);
@@ -101,7 +96,7 @@ describe('Feature flag table', () => {
});
it('Should render a status column', () => {
- const badge = wrapper.find('[data-testid="feature-flag-status-badge"]');
+ const badge = wrapper.findByTestId('feature-flag-status-badge');
expect(badge.exists()).toBe(true);
expect(trimText(badge.text())).toEqual('Active');
@@ -116,10 +111,10 @@ describe('Feature flag table', () => {
);
});
- it('should render an environments specs badge with active class', () => {
- const envColumn = wrapper.find('.js-feature-flag-environments');
+ it('should render an environments specs label', () => {
+ const strategyLabel = wrapper.findByTestId('strategy-label');
- expect(trimText(envColumn.findComponent(GlBadge).text())).toBe('All Users: All Environments');
+ expect(trimText(strategyLabel.text())).toBe('All Users: All Environments');
});
it('should render an actions column', () => {
@@ -167,29 +162,29 @@ describe('Feature flag table', () => {
});
it('shows All Environments if the environment scope is *', () => {
- expect(badges.at(0).text()).toContain('All Environments');
+ expect(labels.at(0).text()).toContain('All Environments');
});
it('shows the environment scope if another is set', () => {
- expect(badges.at(1).text()).toContain('production');
- expect(badges.at(1).text()).toContain('staging');
- expect(badges.at(2).text()).toContain('review/*');
+ expect(labels.at(1).text()).toContain('production');
+ expect(labels.at(1).text()).toContain('staging');
+ expect(labels.at(2).text()).toContain('review/*');
});
it('shows All Users for the default strategy', () => {
- expect(badges.at(0).text()).toContain('All Users');
+ expect(labels.at(0).text()).toContain('All Users');
});
it('shows the percent for a percent rollout', () => {
- expect(badges.at(1).text()).toContain('Percent of users - 50%');
+ expect(labels.at(1).text()).toContain('Percent of users - 50%');
});
it('shows the number of users for users with ID', () => {
- expect(badges.at(2).text()).toContain('User IDs - 4 users');
+ expect(labels.at(2).text()).toContain('User IDs - 4 users');
});
it('shows the name of a user list for user list', () => {
- expect(badges.at(3).text()).toContain('User List - test list');
+ expect(labels.at(3).text()).toContain('User List - test list');
});
it('renders a feature flag without an iid', () => {
diff --git a/spec/frontend/feature_flags/components/strategy_label_spec.js b/spec/frontend/feature_flags/components/strategy_label_spec.js
new file mode 100644
index 00000000000..c2d5ce10448
--- /dev/null
+++ b/spec/frontend/feature_flags/components/strategy_label_spec.js
@@ -0,0 +1,61 @@
+import { mount } from '@vue/test-utils';
+import StrategyLabel from '~/feature_flags/components/strategy_label.vue';
+
+const DEFAULT_PROPS = {
+ name: 'All Users',
+ parameters: 'parameters',
+ scopes: 'scope1, scope2',
+};
+
+describe('feature_flags/components/feature_flags_tab.vue', () => {
+ let wrapper;
+
+ const factory = (props = {}) =>
+ mount(
+ {
+ components: {
+ StrategyLabel,
+ },
+ render(h) {
+ return h(StrategyLabel, { props: this.$attrs, on: this.$listeners }, this.$slots.default);
+ },
+ },
+ {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ },
+ );
+
+ describe('render', () => {
+ let strategyLabel;
+
+ beforeEach(() => {
+ wrapper = factory({});
+ strategyLabel = wrapper.findComponent(StrategyLabel);
+ });
+
+ it('should show the strategy label with parameters and scope', () => {
+ expect(strategyLabel.text()).toContain(DEFAULT_PROPS.name);
+ expect(strategyLabel.text()).toContain(DEFAULT_PROPS.parameters);
+ expect(strategyLabel.text()).toContain(DEFAULT_PROPS.scopes);
+ expect(strategyLabel.text()).toContain('All Users - parameters: scope1, scope2');
+ });
+ });
+
+ describe('without parameters', () => {
+ let strategyLabel;
+
+ beforeEach(() => {
+ wrapper = factory({ parameters: null });
+ strategyLabel = wrapper.findComponent(StrategyLabel);
+ });
+
+ it('should hide empty params and dash', () => {
+ expect(strategyLabel.text()).toContain(DEFAULT_PROPS.name);
+ expect(strategyLabel.text()).not.toContain(' - ');
+ expect(strategyLabel.text()).toContain('All Users: scope1, scope2');
+ });
+ });
+});
diff --git a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
index 22bac3fca15..d82081041d9 100644
--- a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import httpStatusCodes, { HTTP_STATUS_CREATED } from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -11,7 +11,7 @@ describe('feature highlight helper', () => {
let mockAxios;
const endpoint = '/-/callouts/dismiss';
const highlightId = '123';
- const { CREATED, INTERNAL_SERVER_ERROR } = httpStatusCodes;
+ const { INTERNAL_SERVER_ERROR } = httpStatusCodes;
beforeEach(() => {
mockAxios = new MockAdapter(axios);
@@ -22,7 +22,7 @@ describe('feature highlight helper', () => {
});
it('calls persistent dismissal endpoint with highlightId', async () => {
- mockAxios.onPost(endpoint, { feature_name: highlightId }).replyOnce(CREATED);
+ mockAxios.onPost(endpoint, { feature_name: highlightId }).replyOnce(HTTP_STATUS_CREATED);
await expect(dismiss(endpoint, highlightId)).resolves.toEqual(expect.anything());
});
diff --git a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
index 91457f10bf8..ebed477fa2f 100644
--- a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -2,6 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue';
import eventHub from '~/filtered_search/event_hub';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import { TOKEN_TYPE_AUTHOR } from '~/vue_shared/components/filtered_search_bar/constants';
describe('Recent Searches Dropdown Content', () => {
let wrapper;
@@ -60,7 +61,7 @@ describe('Recent Searches Dropdown Content', () => {
items: [
'foo',
'author:@root label:~foo bar',
- [{ type: 'author_username', value: { data: 'toby', operator: '=' } }],
+ [{ type: TOKEN_TYPE_AUTHOR, value: { data: 'toby', operator: '=' } }],
],
isLocalStorageAvailable: true,
});
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
index 5e68725c03e..26af7af701b 100644
--- a/spec/frontend/filtered_search/filtered_search_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -8,7 +8,7 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes';
import { visitUrl, getParameterByName } from '~/lib/utils/url_utility';
@@ -130,14 +130,14 @@ describe('Filtered Search Manager', () => {
manager = new FilteredSearchManager({ page });
});
- it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => {
+ it('should not show an alert if an RecentSearchesServiceError is caught', () => {
jest
.spyOn(RecentSearchesService.prototype, 'fetch')
.mockImplementation(() => Promise.reject(new RecentSearchesServiceError()));
manager.setup();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
index 0e5c94edd05..28fcf0b7ec7 100644
--- a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
@@ -4,6 +4,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import waitForPromises from 'helpers/wait_for_promises';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
describe('Filtered Search Visual Tokens', () => {
let mock;
@@ -302,7 +303,7 @@ describe('Filtered Search Visual Tokens', () => {
});
const token = tokensContainer.querySelector('.js-visual-token');
- expect(token.classList.contains('filtered-search-term')).toEqual(true);
+ expect(token.classList.contains(FILTERED_SEARCH_TERM)).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('search term');
expect(token.querySelector('.operator').innerText).toEqual('=');
expect(token.querySelector('.value')).toEqual(null);
@@ -430,7 +431,7 @@ describe('Filtered Search Visual Tokens', () => {
subject.addSearchVisualToken('search term');
const token = tokensContainer.querySelector('.js-visual-token');
- expect(token.classList.contains('filtered-search-term')).toEqual(true);
+ expect(token.classList.contains(FILTERED_SEARCH_TERM)).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('search term');
expect(token.querySelector('.value')).toEqual(null);
});
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index e52ffa7bd9f..43c10090739 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -5,7 +5,7 @@ import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import { TEST_HOST } from 'helpers/test_constants';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import VisualTokenValue from '~/filtered_search/visual_token_value';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
@@ -61,7 +61,7 @@ describe('Filtered Search Visual Tokens', () => {
};
await subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue);
- expect(createFlash.mock.calls.length).toBe(0);
+ expect(createAlert).toHaveBeenCalledTimes(0);
});
it('does nothing if user cannot be found', async () => {
diff --git a/spec/frontend/fixtures/api_merge_requests.rb b/spec/frontend/fixtures/api_merge_requests.rb
index fae1f4056fb..a71a41dc5c4 100644
--- a/spec/frontend/fixtures/api_merge_requests.rb
+++ b/spec/frontend/fixtures/api_merge_requests.rb
@@ -6,7 +6,6 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include JavaScriptFixturesHelpers
- let_it_be(:admin) { create(:admin, name: 'root') }
let_it_be(:namespace) { create(:namespace, name: 'gitlab-test') }
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
let_it_be(:early_mrs) do
@@ -14,21 +13,22 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
end
let_it_be(:mr) { create(:merge_request, source_project: project) }
+ let_it_be(:user) { project.owner }
it 'api/merge_requests/get.json' do
- get api("/projects/#{project.id}/merge_requests", admin)
+ get api("/projects/#{project.id}/merge_requests", user)
expect(response).to be_successful
end
it 'api/merge_requests/versions.json' do
- get api("/projects/#{project.id}/merge_requests/#{mr.iid}/versions", admin)
+ get api("/projects/#{project.id}/merge_requests/#{mr.iid}/versions", user)
expect(response).to be_successful
end
it 'api/merge_requests/changes.json' do
- get api("/projects/#{project.id}/merge_requests/#{mr.iid}/changes", admin)
+ get api("/projects/#{project.id}/merge_requests/#{mr.iid}/changes", user)
expect(response).to be_successful
end
diff --git a/spec/frontend/fixtures/api_projects.rb b/spec/frontend/fixtures/api_projects.rb
index b14f402a7b9..d1dfd223419 100644
--- a/spec/frontend/fixtures/api_projects.rb
+++ b/spec/frontend/fixtures/api_projects.rb
@@ -6,25 +6,25 @@ RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include JavaScriptFixturesHelpers
- let(:admin) { create(:admin, name: 'root') }
let(:namespace) { create(:namespace, name: 'gitlab-test') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
let(:project_empty) { create(:project_empty_repo, namespace: namespace, path: 'lorem-ipsum-empty') }
+ let(:user) { project.owner }
it 'api/projects/get.json' do
- get api("/projects/#{project.id}", admin)
+ get api("/projects/#{project.id}", user)
expect(response).to be_successful
end
it 'api/projects/get_empty.json' do
- get api("/projects/#{project_empty.id}", admin)
+ get api("/projects/#{project_empty.id}", user)
expect(response).to be_successful
end
it 'api/projects/branches/get.json' do
- get api("/projects/#{project.id}/repository/branches/#{project.default_branch}", admin)
+ get api("/projects/#{project.id}/repository/branches/#{project.default_branch}", user)
expect(response).to be_successful
end
diff --git a/spec/frontend/fixtures/environments.rb b/spec/frontend/fixtures/environments.rb
new file mode 100644
index 00000000000..3ca5b50ac9c
--- /dev/null
+++ b/spec/frontend/fixtures/environments.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Environments (JavaScript fixtures)', feature_category: :environment_management do
+ include ApiHelpers
+ include JavaScriptFixturesHelpers
+ include GraphqlHelpers
+
+ let_it_be(:admin) { create(:admin, username: 'administrator', email: 'admin@example.gitlab.com') }
+ let_it_be(:group) { create(:group, path: 'environments-group') }
+ let_it_be(:project) { create(:project, :repository, group: group, path: 'environments-project') }
+
+ let_it_be(:environment) { create(:environment, name: 'staging', project: project) }
+
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
+
+ let(:user) { create(:user) }
+ let(:role) { :developer }
+ let_it_be(:deployment) do
+ create(:deployment, :success, environment: environment, deployable: nil)
+ end
+
+ let_it_be(:deployment_success) do
+ create(:deployment, :success, environment: environment, deployable: build)
+ end
+
+ let_it_be(:deployment_failed) do
+ create(:deployment, :failed, environment: environment, deployable: build)
+ end
+
+ let_it_be(:deployment_running) do
+ create(:deployment, :running, environment: environment, deployable: build)
+ end
+
+ describe GraphQL::Query, type: :request do
+ environment_details_query_path = 'environments/graphql/queries/environment_details.query.graphql'
+
+ it "graphql/#{environment_details_query_path}.json" do
+ query = get_graphql_query_as_string(environment_details_query_path)
+
+ post_graphql(query, current_user: admin,
+ variables:
+ {
+ projectFullPath: project.full_path,
+ environmentName: environment.name,
+ pageSize: 10
+ })
+ expect_graphql_errors_to_be_empty
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/freeze_period.rb b/spec/frontend/fixtures/freeze_period.rb
index 5aa466ef015..a1c7564d36e 100644
--- a/spec/frontend/fixtures/freeze_period.rb
+++ b/spec/frontend/fixtures/freeze_period.rb
@@ -13,15 +13,6 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
remove_repository(project)
end
- around do |example|
- freeze_time do
- # Mock time to sept 19 (intl. talk like a pirate day)
- travel_to(Time.utc(2020, 9, 19))
-
- example.run
- end
- end
-
describe API::FreezePeriods, '(JavaScript fixtures)', type: :request do
include ApiHelpers
diff --git a/spec/frontend/fixtures/releases.rb b/spec/frontend/fixtures/releases.rb
index fc344472588..c7e3d8fe804 100644
--- a/spec/frontend/fixtures/releases.rb
+++ b/spec/frontend/fixtures/releases.rb
@@ -6,9 +6,9 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
include ApiHelpers
include JavaScriptFixturesHelpers
- let_it_be(:admin) { create(:admin, username: 'administrator', email: 'admin@example.gitlab.com') }
let_it_be(:namespace) { create(:namespace, path: 'releases-namespace') }
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'releases-project') }
+ let_it_be(:user) { create(:user, email: 'user@example.gitlab.com', username: 'user1') }
let_it_be(:milestone_12_3) do
create(:milestone,
@@ -52,7 +52,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
project: project,
tag: 'v1.1',
name: 'The first release',
- author: admin,
+ author: user,
description: 'Best. Release. **Ever.** :rocket:',
created_at: Time.zone.parse('2018-12-3'),
released_at: Time.zone.parse('2018-12-10'))
@@ -105,19 +105,23 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
project: project,
tag: 'v1.2',
name: 'The second release',
- author: admin,
+ author: user,
description: 'An okay release :shrug:',
created_at: Time.zone.parse('2019-01-03'),
released_at: Time.zone.parse('2019-01-10'))
end
+ before do
+ project.add_owner(user)
+ end
+
after(:all) do
remove_repository(project)
end
describe API::Releases, type: :request do
it 'api/releases/release.json' do
- get api("/projects/#{project.id}/releases/#{release.tag}", admin)
+ get api("/projects/#{project.id}/releases/#{release.tag}", user)
expect(response).to be_successful
end
@@ -133,7 +137,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
it "graphql/#{all_releases_query_path}.json" do
query = get_graphql_query_as_string(all_releases_query_path)
- post_graphql(query, current_user: admin, variables: { fullPath: project.full_path })
+ post_graphql(query, current_user: user, variables: { fullPath: project.full_path })
expect_graphql_errors_to_be_empty
expect(graphql_data_at(:project, :releases)).to be_present
@@ -142,7 +146,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
it "graphql/#{one_release_query_path}.json" do
query = get_graphql_query_as_string(one_release_query_path)
- post_graphql(query, current_user: admin, variables: { fullPath: project.full_path, tagName: release.tag })
+ post_graphql(query, current_user: user, variables: { fullPath: project.full_path, tagName: release.tag })
expect_graphql_errors_to_be_empty
expect(graphql_data_at(:project, :release)).to be_present
@@ -151,7 +155,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
it "graphql/#{one_release_for_editing_query_path}.json" do
query = get_graphql_query_as_string(one_release_for_editing_query_path)
- post_graphql(query, current_user: admin, variables: { fullPath: project.full_path, tagName: release.tag })
+ post_graphql(query, current_user: user, variables: { fullPath: project.full_path, tagName: release.tag })
expect_graphql_errors_to_be_empty
expect(graphql_data_at(:project, :release)).to be_present
diff --git a/spec/frontend/fixtures/runner_instructions.rb b/spec/frontend/fixtures/runner_instructions.rb
new file mode 100644
index 00000000000..90a01c37479
--- /dev/null
+++ b/spec/frontend/fixtures/runner_instructions.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Runner Instructions (JavaScript fixtures)', feature_category: :runner do
+ include ApiHelpers
+ include JavaScriptFixturesHelpers
+ include GraphqlHelpers
+
+ query_path = 'vue_shared/components/runner_instructions/graphql/queries'
+
+ describe GraphQL::Query do
+ describe 'get_runner_platforms.query.graphql', type: :request do
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}/get_runner_platforms.query.graphql")
+ end
+
+ it 'graphql/runner_instructions/get_runner_platforms.query.graphql.json' do
+ post_graphql(query)
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ describe 'get_runner_setup.query.graphql', type: :request do
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}/get_runner_setup.query.graphql")
+ end
+
+ it 'graphql/runner_instructions/get_runner_setup.query.graphql.json' do
+ post_graphql(query, variables: { platform: 'linux', architecture: 'amd64' })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it 'graphql/runner_instructions/get_runner_setup.query.graphql.windows.json' do
+ post_graphql(query, variables: { platform: 'windows', architecture: 'amd64' })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/tabs.rb b/spec/frontend/fixtures/tabs.rb
index 697ff1c7c20..57ecb32e289 100644
--- a/spec/frontend/fixtures/tabs.rb
+++ b/spec/frontend/fixtures/tabs.rb
@@ -11,14 +11,14 @@ RSpec.describe 'GlTabsBehavior', '(JavaScript fixtures)', type: :helper do
it 'tabs/tabs.html' do
tabs = gl_tabs_nav({ data: { testid: 'tabs' } }) do
gl_tab_link_to('Foo', '#foo', item_active: true, data: { testid: 'foo-tab' }) +
- gl_tab_link_to('Bar', '#bar', item_active: false, data: { testid: 'bar-tab' }) +
- gl_tab_link_to('Qux', '#qux', item_active: false, data: { testid: 'qux-tab' })
+ gl_tab_link_to('Bar', '#bar', item_active: false, data: { testid: 'bar-tab' }) +
+ gl_tab_link_to('Qux', '#qux', item_active: false, data: { testid: 'qux-tab' })
end
panels = content_tag(:div, class: 'tab-content') do
content_tag(:div, 'Foo', { id: 'foo', class: 'tab-pane active', data: { testid: 'foo-panel' } }) +
- content_tag(:div, 'Bar', { id: 'bar', class: 'tab-pane', data: { testid: 'bar-panel' } }) +
- content_tag(:div, 'Qux', { id: 'qux', class: 'tab-pane', data: { testid: 'qux-panel' } })
+ content_tag(:div, 'Bar', { id: 'bar', class: 'tab-pane', data: { testid: 'bar-panel' } }) +
+ content_tag(:div, 'Qux', { id: 'qux', class: 'tab-pane', data: { testid: 'qux-panel' } })
end
@tabs = tabs + panels
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index a105b0b165c..ade36cd1637 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -12,6 +12,9 @@ import createFlash, {
jest.mock('@sentry/browser');
describe('Flash', () => {
+ const findTextContent = (containerSelector = '.flash-container') =>
+ document.querySelector(containerSelector).textContent.replace(/\s+/g, ' ').trim();
+
describe('hideFlash', () => {
let el;
@@ -99,7 +102,7 @@ describe('Flash', () => {
it('adds alert element into the document by default', () => {
alert = createAlert({ message: mockMessage });
- expect(document.querySelector('.flash-container').textContent.trim()).toBe(mockMessage);
+ expect(findTextContent()).toBe(mockMessage);
expect(document.querySelector('.flash-container .gl-alert')).not.toBeNull();
});
@@ -202,8 +205,7 @@ describe('Flash', () => {
message: mockMessage,
});
- const text = document.querySelector('.flash-container').textContent.trim();
- expect(text).toBe(`${mockTitle} ${mockMessage}`);
+ expect(findTextContent()).toBe(`${mockTitle} ${mockMessage}`);
});
});
@@ -319,6 +321,22 @@ describe('Flash', () => {
});
});
});
+
+ describe('when called multiple times', () => {
+ it('clears previous alerts', () => {
+ createAlert({ message: 'message 1' });
+ createAlert({ message: 'message 2' });
+
+ expect(findTextContent()).toBe('message 2');
+ });
+
+ it('preserves alerts when `preservePrevious` is true', () => {
+ createAlert({ message: 'message 1' });
+ createAlert({ message: 'message 2', preservePrevious: true });
+
+ expect(findTextContent()).toBe('message 1 message 2');
+ });
+ });
});
});
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 68225f39c66..eeef92d4183 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -772,6 +772,7 @@ describe('GfmAutoComplete', () => {
input | output
${'~'} | ${unassignedLabels}
${'/label ~'} | ${unassignedLabels}
+ ${'/labels ~'} | ${unassignedLabels}
${'/relabel ~'} | ${unassignedLabels}
${'/unlabel ~'} | ${[]}
`('$input shows $output.length labels', expectLabels);
@@ -786,6 +787,7 @@ describe('GfmAutoComplete', () => {
input | output
${'~'} | ${allLabels}
${'/label ~'} | ${unassignedLabels}
+ ${'/labels ~'} | ${unassignedLabels}
${'/relabel ~'} | ${allLabels}
${'/unlabel ~'} | ${assignedLabels}
`('$input shows $output.length labels', expectLabels);
@@ -800,6 +802,7 @@ describe('GfmAutoComplete', () => {
input | output
${'~'} | ${assignedLabels}
${'/label ~'} | ${[]}
+ ${'/labels ~'} | ${[]}
${'/relabel ~'} | ${assignedLabels}
${'/unlabel ~'} | ${assignedLabels}
`('$input shows $output.length labels', expectLabels);
diff --git a/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js
new file mode 100644
index 00000000000..f1ed32a5f79
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js
@@ -0,0 +1,202 @@
+import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { sprintf } from '~/locale';
+import SecurityPatchUpgradeAlertModal from '~/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue';
+import * as utils from '~/gitlab_version_check/utils';
+import {
+ UPGRADE_DOCS_URL,
+ ABOUT_RELEASES_PAGE,
+ TRACKING_ACTIONS,
+ TRACKING_LABELS,
+} from '~/gitlab_version_check/constants';
+
+describe('SecurityPatchUpgradeAlertModal', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const defaultProps = {
+ currentVersion: '11.1.1',
+ };
+
+ const createComponent = (props = {}) => {
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+
+ wrapper = shallowMountExtended(SecurityPatchUpgradeAlertModal, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlModal,
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ const expectDispatchedTracking = (action, label) => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, action, {
+ label,
+ property: defaultProps.currentVersion,
+ });
+ };
+
+ const findGlModal = () => wrapper.findComponent(GlModal);
+ const findGlModalTitle = () => wrapper.findByTestId('alert-modal-title');
+ const findGlModalBody = () => wrapper.findByTestId('alert-modal-body');
+ const findGlModalDetails = () => wrapper.findByTestId('alert-modal-details');
+ const findGlLink = () => wrapper.findComponent(GlLink);
+ const findGlRemindButton = () => wrapper.findByTestId('alert-modal-remind-button');
+ const findGlUpgradeButton = () => wrapper.findByTestId('alert-modal-upgrade-button');
+
+ describe('template defaults', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders visible critical security alert modal', () => {
+ expect(findGlModal().props('visible')).toBe(true);
+ });
+
+ it('renders the modal title correctly', () => {
+ expect(findGlModalTitle().text()).toBe(wrapper.vm.$options.i18n.modalTitle);
+ });
+
+ it('renders modal body without suggested versions', () => {
+ expect(findGlModalBody().text()).toBe(
+ sprintf(wrapper.vm.$options.i18n.modalBodyNoStableVersions, {
+ currentVersion: defaultProps.currentVersion,
+ }),
+ );
+ });
+
+ it('does not render modal details', () => {
+ expect(findGlModalDetails().exists()).toBe(false);
+ });
+
+ it(`tracks render ${TRACKING_LABELS.MODAL} correctly`, () => {
+ expectDispatchedTracking(TRACKING_ACTIONS.RENDER, TRACKING_LABELS.MODAL);
+ });
+
+ it(`tracks click ${TRACKING_LABELS.DISMISS} when close button clicked`, async () => {
+ await findGlModal().vm.$emit('close');
+
+ expectDispatchedTracking(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.DISMISS);
+ });
+
+ describe('Learn more link', () => {
+ it('renders with correct text and link', () => {
+ expect(findGlLink().text()).toBe(wrapper.vm.$options.i18n.learnMore);
+ expect(findGlLink().attributes('href')).toBe(ABOUT_RELEASES_PAGE);
+ });
+
+ it(`tracks click ${TRACKING_LABELS.LEARN_MORE_LINK} when clicked`, async () => {
+ await findGlLink().vm.$emit('click');
+
+ expectDispatchedTracking(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.LEARN_MORE_LINK);
+ });
+ });
+
+ describe('Remind me button', () => {
+ beforeEach(() => {
+ wrapper.vm.$refs.alertModal.hide = jest.fn();
+ });
+
+ it('renders with correct text', () => {
+ expect(findGlRemindButton().text()).toBe(wrapper.vm.$options.i18n.secondaryButtonText);
+ });
+
+ it(`tracks click ${TRACKING_LABELS.REMIND_ME_BTN} when clicked`, async () => {
+ await findGlRemindButton().vm.$emit('click');
+
+ expectDispatchedTracking(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.REMIND_ME_BTN);
+ });
+
+ it('calls setHideAlertModalCookie with the currentVersion when clicked', async () => {
+ jest.spyOn(utils, 'setHideAlertModalCookie');
+ await findGlRemindButton().vm.$emit('click');
+
+ expect(utils.setHideAlertModalCookie).toHaveBeenCalledWith(defaultProps.currentVersion);
+ });
+
+ it('hides the modal', async () => {
+ await findGlRemindButton().vm.$emit('click');
+
+ expect(wrapper.vm.$refs.alertModal.hide).toHaveBeenCalled();
+ });
+ });
+
+ describe('Upgrade button', () => {
+ it('renders with correct text and link', () => {
+ expect(findGlUpgradeButton().text()).toBe(wrapper.vm.$options.i18n.primaryButtonText);
+ expect(findGlUpgradeButton().attributes('href')).toBe(UPGRADE_DOCS_URL);
+ });
+
+ it(`tracks click ${TRACKING_LABELS.UPGRADE_BTN_LINK} when clicked`, async () => {
+ await findGlUpgradeButton().vm.$emit('click');
+
+ expectDispatchedTracking(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.UPGRADE_BTN_LINK);
+ });
+
+ it('calls setHideAlertModalCookie with the currentVersion when clicked', async () => {
+ jest.spyOn(utils, 'setHideAlertModalCookie');
+ await findGlUpgradeButton().vm.$emit('click');
+
+ expect(utils.setHideAlertModalCookie).toHaveBeenCalledWith(defaultProps.currentVersion);
+ });
+ });
+ });
+
+ describe('template with latestStableVersions', () => {
+ const latestStableVersions = ['88.8.3', '89.9.9', '90.0.0'];
+
+ beforeEach(() => {
+ createComponent({ latestStableVersions });
+ });
+
+ it('renders modal body with suggested versions', () => {
+ expect(findGlModalBody().text()).toBe(
+ sprintf(wrapper.vm.$options.i18n.modalBodyStableVersions, {
+ currentVersion: defaultProps.currentVersion,
+ latestStableVersions: latestStableVersions.join(', '),
+ }),
+ );
+ });
+ });
+
+ describe('template with details', () => {
+ const details = 'This is some details about the upgrade';
+
+ beforeEach(() => {
+ createComponent({ details });
+ });
+
+ it('renders modal details', () => {
+ expect(findGlModalDetails().text()).toBe(
+ sprintf(wrapper.vm.$options.i18n.modalDetails, { details }),
+ );
+ });
+ });
+
+ describe('when modal is hidden by cookie', () => {
+ beforeEach(() => {
+ jest.spyOn(utils, 'getHideAlertModalCookie').mockReturnValue(true);
+ createComponent();
+ });
+
+ it('renders modal with visibility false', () => {
+ expect(findGlModal().props('visible')).toBe(false);
+ });
+
+ it(`does not track render ${TRACKING_LABELS.MODAL} correctly`, () => {
+ expect(trackingSpy).not.toHaveBeenCalledWith(undefined, TRACKING_ACTIONS.RENDER, {
+ label: TRACKING_LABELS.MODAL,
+ property: defaultProps.currentVersion,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_spec.js b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_spec.js
new file mode 100644
index 00000000000..665dacd5c47
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_spec.js
@@ -0,0 +1,84 @@
+import { GlAlert, GlButton, GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import SecurityPatchUpgradeAlert from '~/gitlab_version_check/components/security_patch_upgrade_alert.vue';
+import { UPGRADE_DOCS_URL, ABOUT_RELEASES_PAGE } from '~/gitlab_version_check/constants';
+
+describe('SecurityPatchUpgradeAlert', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const defaultProps = {
+ currentVersion: '99.9',
+ };
+
+ const createComponent = () => {
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+
+ wrapper = shallowMount(SecurityPatchUpgradeAlert, {
+ propsData: {
+ ...defaultProps,
+ },
+ stubs: {
+ GlAlert,
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ const findGlAlert = () => wrapper.findComponent(GlAlert);
+ const findGlButton = () => wrapper.findComponent(GlButton);
+ const findGlLink = () => wrapper.findComponent(GlLink);
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders non-dismissible GlAlert with version information', () => {
+ expect(findGlAlert().text()).toContain(
+ `You are currently on version ${defaultProps.currentVersion}.`,
+ );
+ expect(findGlAlert().props('dismissible')).toBe(false);
+ });
+
+ it('tracks render security_patch_upgrade_alert correctly', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
+ label: 'security_patch_upgrade_alert',
+ property: defaultProps.currentVersion,
+ });
+ });
+
+ it('renders GlLink with correct text and link', () => {
+ expect(findGlLink().text()).toBe('Learn more about this critical security release.');
+ expect(findGlLink().attributes('href')).toBe(ABOUT_RELEASES_PAGE);
+ });
+
+ it('tracks click security_patch_upgrade_alert_learn_more when link is clicked', async () => {
+ await findGlLink().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
+ label: 'security_patch_upgrade_alert_learn_more',
+ property: defaultProps.currentVersion,
+ });
+ });
+
+ it('renders GlButton with correct text and link', () => {
+ expect(findGlButton().text()).toBe('Upgrade now');
+ expect(findGlButton().attributes('href')).toBe(UPGRADE_DOCS_URL);
+ });
+
+ it('tracks click security_patch_upgrade_alert_upgrade_now when button is clicked', async () => {
+ await findGlButton().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
+ label: 'security_patch_upgrade_alert_upgrade_now',
+ property: defaultProps.currentVersion,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/gitlab_version_check/index_spec.js b/spec/frontend/gitlab_version_check/index_spec.js
index 8a11ff48bf2..92bc103cede 100644
--- a/spec/frontend/gitlab_version_check/index_spec.js
+++ b/spec/frontend/gitlab_version_check/index_spec.js
@@ -1,116 +1,52 @@
-import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
+import { createWrapper } from '@vue/test-utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initGitlabVersionCheck from '~/gitlab_version_check';
+import {
+ VERSION_CHECK_BADGE_NO_PROP_FIXTURE,
+ VERSION_CHECK_BADGE_NO_SEVERITY_FIXTURE,
+ VERSION_CHECK_BADGE_FIXTURE,
+ VERSION_CHECK_BADGE_FINDER,
+ VERSION_BADGE_TEXT,
+ SECURITY_PATCH_FIXTURE,
+ SECURITY_PATCH_FINDER,
+ SECURITY_PATCH_TEXT,
+ SECURITY_MODAL_FIXTURE,
+ SECURITY_MODAL_FINDER,
+ SECURITY_MODAL_TEXT,
+} from './mock_data';
describe('initGitlabVersionCheck', () => {
- let originalGon;
- let mock;
- let vueApps;
+ let wrapper;
- const defaultResponse = {
- code: 200,
- res: { severity: 'success' },
- };
-
- const dummyGon = {
- relative_url_root: '/',
- };
-
- const createApp = async (mockResponse, htmlClass) => {
- originalGon = window.gon;
-
- const response = {
- ...defaultResponse,
- ...mockResponse,
- };
-
- mock = new MockAdapter(axios);
- mock.onGet().replyOnce(response.code, response.res);
-
- setHTMLFixture(`<div class="${htmlClass}"></div>`);
-
- vueApps = await initGitlabVersionCheck();
+ const createApp = (fixture) => {
+ setHTMLFixture(fixture);
+ initGitlabVersionCheck();
+ wrapper = createWrapper(document.body);
};
afterEach(() => {
- mock.restore();
- window.gon = originalGon;
resetHTMLFixture();
});
- describe('with no .js-gitlab-version-check-badge elements', () => {
- beforeEach(async () => {
- await createApp();
- });
-
- it('does not make axios GET request', () => {
- expect(mock.history.get.length).toBe(0);
- });
-
- it('does not render the Version Check Badge', () => {
- expect(vueApps).toBeNull();
- });
- });
-
- describe('with .js-gitlab-version-check-badge element but API errors', () => {
- beforeEach(async () => {
- jest.spyOn(Sentry, 'captureException');
- await createApp({ code: 500, res: null }, 'js-gitlab-version-check-badge');
- });
-
- it('does make axios GET request', () => {
- expect(mock.history.get.length).toBe(1);
- expect(mock.history.get[0].url).toContain('/admin/version_check.json');
- });
-
- it('logs error to Sentry', () => {
- expect(Sentry.captureException).toHaveBeenCalled();
- });
-
- it('does not render the Version Check Badge', () => {
- expect(vueApps).toBeNull();
- });
- });
-
- describe('with .js-gitlab-version-check-badge element and successful API call', () => {
- beforeEach(async () => {
- await createApp({}, 'js-gitlab-version-check-badge');
- });
-
- it('does make axios GET request', () => {
- expect(mock.history.get.length).toBe(1);
- expect(mock.history.get[0].url).toContain('/admin/version_check.json');
- });
-
- it('does render the Version Check Badge', () => {
- expect(vueApps).toHaveLength(1);
- expect(vueApps[0]).toBeInstanceOf(Vue);
- });
- });
-
describe.each`
- root | description
- ${'/'} | ${'not used (uses its own (sub)domain)'}
- ${'/gitlab'} | ${'custom path'}
- ${'/service/gitlab'} | ${'custom path with 2 depth'}
- `('path for version_check.json', ({ root, description }) => {
- describe(`when relative url is ${description}: ${root}`, () => {
- beforeEach(async () => {
- originalGon = window.gon;
- window.gon = { ...dummyGon };
- window.gon.relative_url_root = root;
- await createApp({}, 'js-gitlab-version-check-badge');
- });
-
- it('reflects the relative url setting', () => {
- expect(mock.history.get.length).toBe(1);
-
- const pathRegex = new RegExp(`^${root}`);
- expect(mock.history.get[0].url).toMatch(pathRegex);
- });
+ description | fixture | finders | componentTexts
+ ${'with no version check elements'} | ${'<div></div>'} | ${[]} | ${[]}
+ ${'with version check badge el but no prop data'} | ${VERSION_CHECK_BADGE_NO_PROP_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[undefined]}
+ ${'with version check badge el but no severity data'} | ${VERSION_CHECK_BADGE_NO_SEVERITY_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[undefined]}
+ ${'with version check badge el and version data'} | ${VERSION_CHECK_BADGE_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[VERSION_BADGE_TEXT]}
+ ${'with security patch el'} | ${SECURITY_PATCH_FIXTURE} | ${[SECURITY_PATCH_FINDER]} | ${[SECURITY_PATCH_TEXT]}
+ ${'with security patch and version badge els'} | ${`${SECURITY_PATCH_FIXTURE}${VERSION_CHECK_BADGE_FIXTURE}`} | ${[SECURITY_PATCH_FINDER, VERSION_CHECK_BADGE_FINDER]} | ${[SECURITY_PATCH_TEXT, VERSION_BADGE_TEXT]}
+ ${'with security modal el'} | ${SECURITY_MODAL_FIXTURE} | ${[SECURITY_MODAL_FINDER]} | ${[SECURITY_MODAL_TEXT]}
+ ${'with security modal, security patch, and version badge els'} | ${`${SECURITY_PATCH_FIXTURE}${SECURITY_MODAL_FIXTURE}${VERSION_CHECK_BADGE_FIXTURE}`} | ${[SECURITY_PATCH_FINDER, SECURITY_MODAL_FINDER, VERSION_CHECK_BADGE_FINDER]} | ${[SECURITY_PATCH_TEXT, SECURITY_MODAL_TEXT, VERSION_BADGE_TEXT]}
+ `('$description', ({ fixture, finders, componentTexts }) => {
+ beforeEach(() => {
+ createApp(fixture);
+ });
+
+ it(`correctly renders the Version Check Components`, () => {
+ const renderedComponentTexts = finders.map((f) => wrapper.find(f)?.element?.innerText.trim());
+
+ expect(renderedComponentTexts).toStrictEqual(componentTexts);
});
});
});
diff --git a/spec/frontend/gitlab_version_check/mock_data.js b/spec/frontend/gitlab_version_check/mock_data.js
new file mode 100644
index 00000000000..707d45550eb
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/mock_data.js
@@ -0,0 +1,22 @@
+export const VERSION_CHECK_BADGE_NO_PROP_FIXTURE =
+ '<div class="js-gitlab-version-check-badge"></div>';
+
+export const VERSION_CHECK_BADGE_NO_SEVERITY_FIXTURE = `<div class="js-gitlab-version-check-badge" data-version='{ "size": "sm" }'></div>`;
+
+export const VERSION_CHECK_BADGE_FIXTURE = `<div class="js-gitlab-version-check-badge" data-version='{ "severity": "success" }'></div>`;
+
+export const VERSION_CHECK_BADGE_FINDER = '[data-testid="badge-click-wrapper"]';
+
+export const VERSION_BADGE_TEXT = 'Up to date';
+
+export const SECURITY_PATCH_FIXTURE = `<div id="js-security-patch-upgrade-alert" data-current-version="15.1"></div>`;
+
+export const SECURITY_PATCH_FINDER = 'h2';
+
+export const SECURITY_PATCH_TEXT = 'Critical security upgrade available';
+
+export const SECURITY_MODAL_FIXTURE = `<div id="js-security-patch-upgrade-alert-modal" data-current-version="15.1" data-version='{ "details": "test details", "latest-stable-versions": "[]" }'></div>`;
+
+export const SECURITY_MODAL_FINDER = '[data-testid="alert-modal-title"]';
+
+export const SECURITY_MODAL_TEXT = 'Important notice - Critical security release';
diff --git a/spec/frontend/gitlab_version_check/utils_spec.js b/spec/frontend/gitlab_version_check/utils_spec.js
new file mode 100644
index 00000000000..6126d88dfec
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/utils_spec.js
@@ -0,0 +1,35 @@
+import { parseBoolean, getCookie, setCookie } from '~/lib/utils/common_utils';
+import { getHideAlertModalCookie, setHideAlertModalCookie } from '~/gitlab_version_check/utils';
+import { COOKIE_EXPIRATION, COOKIE_SUFFIX } from '~/gitlab_version_check/constants';
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ parseBoolean: jest.fn().mockReturnValue(true),
+ getCookie: jest.fn().mockReturnValue('true'),
+ setCookie: jest.fn(),
+}));
+
+describe('GitLab Version Check Utils', () => {
+ describe('setHideAlertModalCookie', () => {
+ it('properly generates a key based on the currentVersion and sets Cookie to `true`', () => {
+ const currentVersion = '99.9.9';
+
+ setHideAlertModalCookie(currentVersion);
+
+ expect(setCookie).toHaveBeenCalledWith(`${currentVersion}${COOKIE_SUFFIX}`, true, {
+ expires: COOKIE_EXPIRATION,
+ });
+ });
+ });
+
+ describe('getHideAlertModalCookie', () => {
+ it('properly generates a key based on the currentVersion, fetches said Cooke, and parsesBoolean it', () => {
+ const currentVersion = '99.9.9';
+
+ const res = getHideAlertModalCookie(currentVersion);
+
+ expect(getCookie).toHaveBeenCalledWith(`${currentVersion}${COOKIE_SUFFIX}`);
+ expect(parseBoolean).toHaveBeenCalledWith('true');
+ expect(res).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 091ec17d58e..140609161d4 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -1,7 +1,7 @@
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import appComponent from '~/groups/components/app.vue';
@@ -10,8 +10,6 @@ import groupItemComponent from '~/groups/components/group_item.vue';
import eventHub from '~/groups/event_hub';
import GroupsService from '~/groups/service/groups_service';
import GroupsStore from '~/groups/store/groups_store';
-import EmptyState from '~/groups/components/empty_state.vue';
-import GroupsComponent from '~/groups/components/groups.vue';
import axios from '~/lib/utils/axios_utils';
import * as urlUtilities from '~/lib/utils/url_utility';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -43,7 +41,7 @@ describe('AppComponent', () => {
const createShallowComponent = ({ propsData = {} } = {}) => {
store.state.pageInfo = mockPageInfo;
- wrapper = shallowMount(appComponent, {
+ wrapper = shallowMountExtended(appComponent, {
propsData: {
store,
service,
@@ -51,6 +49,9 @@ describe('AppComponent', () => {
containerId: 'js-groups-tree',
...propsData,
},
+ scopedSlots: {
+ 'empty-state': '<div data-testid="empty-state" />',
+ },
mocks: {
$toast,
},
@@ -68,6 +69,7 @@ describe('AppComponent', () => {
mock.onGet('/dashboard/groups.json').reply(200, mockGroups);
Vue.component('GroupFolder', groupFolderComponent);
Vue.component('GroupItem', groupItemComponent);
+ setWindowLocation('?filter=foobar');
document.body.innerHTML = `
<div id="js-groups-tree">
@@ -149,13 +151,13 @@ describe('AppComponent', () => {
expect(vm.fetchGroups).toHaveBeenCalledWith({
page: null,
- filterGroupsBy: null,
+ filterGroupsBy: 'foobar',
sortBy: null,
updatePagination: true,
archived: null,
});
return fetchPromise.then(() => {
- expect(vm.updateGroups).toHaveBeenCalled();
+ expect(vm.updateGroups).toHaveBeenCalledWith(mockSearchedGroups, true);
});
});
});
@@ -375,32 +377,16 @@ describe('AppComponent', () => {
expect(vm.store.setSearchedGroups).toHaveBeenCalledWith(mockGroups);
});
- it('should set `isSearchEmpty` prop based on groups count and `filter` query param', () => {
- setWindowLocation('?filter=foobar');
- createShallowComponent();
-
- vm.updateGroups(mockGroups);
-
- expect(vm.isSearchEmpty).toBe(false);
-
- vm.updateGroups([]);
-
- expect(vm.isSearchEmpty).toBe(true);
- });
-
describe.each`
- action | groups | fromSearch | shouldRenderEmptyState | searchEmpty
- ${'subgroups_and_projects'} | ${[]} | ${false} | ${true} | ${false}
- ${''} | ${[]} | ${false} | ${false} | ${false}
- ${'subgroups_and_projects'} | ${mockGroups} | ${false} | ${false} | ${false}
- ${'subgroups_and_projects'} | ${[]} | ${true} | ${false} | ${true}
+ groups | fromSearch | shouldRenderEmptyState | shouldRenderSearchEmptyState
+ ${[]} | ${false} | ${true} | ${false}
+ ${mockGroups} | ${false} | ${false} | ${false}
+ ${[]} | ${true} | ${false} | ${true}
`(
- 'when `action` is $action, `groups` is $groups, and `fromSearch` is $fromSearch',
- ({ action, groups, fromSearch, shouldRenderEmptyState, searchEmpty }) => {
+ 'when `groups` is $groups, and `fromSearch` is $fromSearch',
+ ({ groups, fromSearch, shouldRenderEmptyState, shouldRenderSearchEmptyState }) => {
it(`${shouldRenderEmptyState ? 'renders' : 'does not render'} empty state`, async () => {
- createShallowComponent({
- propsData: { action, renderEmptyState: true },
- });
+ createShallowComponent();
await waitForPromises();
@@ -408,28 +394,14 @@ describe('AppComponent', () => {
await nextTick();
- expect(wrapper.findComponent(EmptyState).exists()).toBe(shouldRenderEmptyState);
- expect(wrapper.findComponent(GroupsComponent).props('searchEmpty')).toBe(searchEmpty);
+ expect(wrapper.findByTestId('empty-state').exists()).toBe(shouldRenderEmptyState);
+ expect(wrapper.findByTestId('search-empty-state').exists()).toBe(
+ shouldRenderSearchEmptyState,
+ );
});
},
);
});
-
- describe('when `action` is subgroups_and_projects, `groups` is [], `fromSearch` is `false`, and `renderEmptyState` is `false`', () => {
- it('renders legacy empty state', async () => {
- createShallowComponent({
- propsData: { action: 'subgroups_and_projects' },
- });
-
- vm.updateGroups([], false);
-
- await nextTick();
-
- expect(
- document.querySelector('[data-testid="legacy-empty-state"]').classList.contains('hidden'),
- ).toBe(false);
- });
- });
});
describe('created', () => {
diff --git a/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js b/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js
new file mode 100644
index 00000000000..be61ffa92b4
--- /dev/null
+++ b/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js
@@ -0,0 +1,27 @@
+import { GlEmptyState } from '@gitlab/ui';
+
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ArchivedProjectsEmptyState from '~/groups/components/empty_states/archived_projects_empty_state.vue';
+
+let wrapper;
+
+const defaultProvide = {
+ newProjectIllustration: '/assets/illustrations/project-create-new-sm.svg',
+};
+
+const createComponent = () => {
+ wrapper = mountExtended(ArchivedProjectsEmptyState, {
+ provide: defaultProvide,
+ });
+};
+
+describe('ArchivedProjectsEmptyState', () => {
+ it('renders empty state', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+ title: ArchivedProjectsEmptyState.i18n.title,
+ svgPath: defaultProvide.newProjectIllustration,
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js b/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js
new file mode 100644
index 00000000000..c4ace1be1f3
--- /dev/null
+++ b/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js
@@ -0,0 +1,27 @@
+import { GlEmptyState } from '@gitlab/ui';
+
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import SharedProjectsEmptyState from '~/groups/components/empty_states/shared_projects_empty_state.vue';
+
+let wrapper;
+
+const defaultProvide = {
+ newProjectIllustration: '/assets/illustrations/project-create-new-sm.svg',
+};
+
+const createComponent = () => {
+ wrapper = mountExtended(SharedProjectsEmptyState, {
+ provide: defaultProvide,
+ });
+};
+
+describe('SharedProjectsEmptyState', () => {
+ it('renders empty state', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+ title: SharedProjectsEmptyState.i18n.title,
+ svgPath: defaultProvide.newProjectIllustration,
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/empty_state_spec.js b/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
index fbeaa32b1ec..75edc602fbf 100644
--- a/spec/frontend/groups/components/empty_state_spec.js
+++ b/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
@@ -1,7 +1,7 @@
import { GlEmptyState } from '@gitlab/ui';
import { mountExtended } from 'jest/__helpers__/vue_test_utils_helper';
-import EmptyState from '~/groups/components/empty_state.vue';
+import SubgroupsAndProjectsEmptyState from '~/groups/components/empty_states/subgroups_and_projects_empty_state.vue';
let wrapper;
@@ -16,7 +16,7 @@ const defaultProvide = {
};
const createComponent = ({ provide = {} } = {}) => {
- wrapper = mountExtended(EmptyState, {
+ wrapper = mountExtended(SubgroupsAndProjectsEmptyState, {
provide: {
...defaultProvide,
...provide,
@@ -30,18 +30,18 @@ afterEach(() => {
const findNewSubgroupLink = () =>
wrapper.findByRole('link', {
- name: new RegExp(EmptyState.i18n.withLinks.subgroup.title),
+ name: new RegExp(SubgroupsAndProjectsEmptyState.i18n.withLinks.subgroup.title),
});
const findNewProjectLink = () =>
wrapper.findByRole('link', {
- name: new RegExp(EmptyState.i18n.withLinks.project.title),
+ name: new RegExp(SubgroupsAndProjectsEmptyState.i18n.withLinks.project.title),
});
const findNewSubgroupIllustration = () =>
- wrapper.findByRole('img', { name: EmptyState.i18n.withLinks.subgroup.title });
+ wrapper.findByRole('img', { name: SubgroupsAndProjectsEmptyState.i18n.withLinks.subgroup.title });
const findNewProjectIllustration = () =>
- wrapper.findByRole('img', { name: EmptyState.i18n.withLinks.project.title });
+ wrapper.findByRole('img', { name: SubgroupsAndProjectsEmptyState.i18n.withLinks.project.title });
-describe('EmptyState', () => {
+describe('SubgroupsAndProjectsEmptyState', () => {
describe('when user has permission to create a subgroup', () => {
it('renders `Create new subgroup` link', () => {
createComponent();
@@ -69,8 +69,8 @@ describe('EmptyState', () => {
createComponent({ provide: { canCreateSubgroups: false, canCreateProjects: false } });
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
- title: EmptyState.i18n.withoutLinks.title,
- description: EmptyState.i18n.withoutLinks.description,
+ title: SubgroupsAndProjectsEmptyState.i18n.withoutLinks.title,
+ description: SubgroupsAndProjectsEmptyState.i18n.withoutLinks.description,
svgPath: defaultProvide.emptySubgroupIllustration,
});
});
diff --git a/spec/frontend/groups/components/group_name_and_path_spec.js b/spec/frontend/groups/components/group_name_and_path_spec.js
index 823d2ed286a..9965b608f27 100644
--- a/spec/frontend/groups/components/group_name_and_path_spec.js
+++ b/spec/frontend/groups/components/group_name_and_path_spec.js
@@ -398,7 +398,7 @@ describe('GroupNameAndPath', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().findByRole('link', { name: 'Learn more' }).attributes('href')).toBe(
- helpPagePath('user/group/index', {
+ helpPagePath('user/group/manage', {
anchor: 'change-a-groups-path',
}),
);
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index 0cbb6cc8309..cae29a8f15a 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -16,7 +16,6 @@ describe('GroupsComponent', () => {
const defaultPropsData = {
groups: mockGroups,
pageInfo: mockPageInfo,
- searchEmpty: false,
};
const createComponent = ({ propsData } = {}) => {
@@ -69,14 +68,5 @@ describe('GroupsComponent', () => {
expect(findPaginationLinks().exists()).toBe(true);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false);
});
-
- it('should render empty search message when `searchEmpty` is `true`', () => {
- createComponent({ propsData: { searchEmpty: true } });
-
- expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
- title: GroupsComponent.i18n.emptyStateTitle,
- description: GroupsComponent.i18n.emptyStateDescription,
- });
- });
});
});
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index b615679dcc5..d1ae2c4be17 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -1,11 +1,13 @@
import { GlSorting, GlSortingItem, GlTab } from '@gitlab/ui';
-import { nextTick } from 'vue';
-import { createLocalVue } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import OverviewTabs from '~/groups/components/overview_tabs.vue';
import GroupsApp from '~/groups/components/app.vue';
import GroupFolderComponent from '~/groups/components/group_folder.vue';
+import SubgroupsAndProjectsEmptyState from '~/groups/components/empty_states/subgroups_and_projects_empty_state.vue';
+import SharedProjectsEmptyState from '~/groups/components/empty_states/shared_projects_empty_state.vue';
+import ArchivedProjectsEmptyState from '~/groups/components/empty_states/archived_projects_empty_state.vue';
import GroupsStore from '~/groups/store/groups_store';
import GroupsService from '~/groups/service/groups_service';
import { createRouter } from '~/groups/init_overview_tabs';
@@ -17,9 +19,9 @@ import {
OVERVIEW_TABS_SORTING_ITEMS,
} from '~/groups/constants';
import axios from '~/lib/utils/axios_utils';
+import waitForPromises from 'helpers/wait_for_promises';
-const localVue = createLocalVue();
-localVue.component('GroupFolder', GroupFolderComponent);
+Vue.component('GroupFolder', GroupFolderComponent);
const router = createRouter();
const [SORTING_ITEM_NAME, , SORTING_ITEM_UPDATED] = OVERVIEW_TABS_SORTING_ITEMS;
@@ -57,7 +59,6 @@ describe('OverviewTabs', () => {
...defaultProvide,
...provide,
},
- localVue,
mocks: { $route: route, $router: routerMock },
});
@@ -71,6 +72,7 @@ describe('OverviewTabs', () => {
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
+ axiosMock.onGet({ data: [] });
});
afterEach(() => {
@@ -78,7 +80,7 @@ describe('OverviewTabs', () => {
axiosMock.restore();
});
- it('renders `Subgroups and projects` tab with `GroupsApp` component', async () => {
+ it('renders `Subgroups and projects` tab with `GroupsApp` component with correct empty state', async () => {
await createComponent();
const tabPanel = findTabPanels().at(0);
@@ -92,11 +94,14 @@ describe('OverviewTabs', () => {
store: new GroupsStore({ showSchemaMarkup: true }),
service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
hideProjects: false,
- renderEmptyState: true,
});
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(SubgroupsAndProjectsEmptyState).exists()).toBe(true);
});
- it('renders `Shared projects` tab and renders `GroupsApp` component after clicking tab', async () => {
+ it('renders `Shared projects` tab and renders `GroupsApp` component with correct empty state after clicking tab', async () => {
await createComponent();
const tabPanel = findTabPanels().at(1);
@@ -113,13 +118,16 @@ describe('OverviewTabs', () => {
store: new GroupsStore(),
service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SHARED]),
hideProjects: false,
- renderEmptyState: false,
});
expect(tabPanel.vm.$attrs.lazy).toBe(false);
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(SharedProjectsEmptyState).exists()).toBe(true);
});
- it('renders `Archived projects` tab and renders `GroupsApp` component after clicking tab', async () => {
+ it('renders `Archived projects` tab and renders `GroupsApp` component with correct empty state after clicking tab', async () => {
await createComponent();
const tabPanel = findTabPanels().at(2);
@@ -136,10 +144,13 @@ describe('OverviewTabs', () => {
store: new GroupsStore(),
service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_ARCHIVED]),
hideProjects: false,
- renderEmptyState: false,
});
expect(tabPanel.vm.$attrs.lazy).toBe(false);
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(ArchivedProjectsEmptyState).exists()).toBe(true);
});
it('sets `lazy` prop to `false` for initially active tab and `true` for all other tabs', async () => {
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index b0bfe2b45f0..c714c269ca0 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -180,7 +180,6 @@ describe('HeaderSearchApp', () => {
findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: 27 }));
await nextTick();
expect(findHeaderSearchDropdown().exists()).toBe(false);
- // only one event emmited from findHeaderSearchInput().vm.$emit('click');
expect(wrapper.emitted().expandSearchBar.length).toBe(1);
});
});
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js
index b7349b8fed1..294f5eee863 100644
--- a/spec/frontend/ide/components/panes/right_spec.js
+++ b/spec/frontend/ide/components/panes/right_spec.js
@@ -3,16 +3,12 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
import RightPane from '~/ide/components/panes/right.vue';
-import SwitchEditorsView from '~/ide/components/switch_editors/switch_editors_view.vue';
import { rightSidebarViews } from '~/ide/constants';
import { createStore } from '~/ide/stores';
import extendStore from '~/ide/stores/extend';
-import { __ } from '~/locale';
Vue.use(Vuex);
-const SWITCH_EDITORS_VIEW_NAME = 'switch-editors';
-
describe('ide/components/panes/right.vue', () => {
let wrapper;
let store;
@@ -45,7 +41,6 @@ describe('ide/components/panes/right.vue', () => {
it('renders collapsible-sidebar', () => {
expect(wrapper.findComponent(CollapsibleSidebar).props()).toMatchObject({
side: 'right',
- initOpenView: SWITCH_EDITORS_VIEW_NAME,
});
});
});
@@ -130,32 +125,4 @@ describe('ide/components/panes/right.vue', () => {
);
});
});
-
- describe('switch editors tab', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it.each`
- desc | canUseNewWebIde | expectedShow
- ${'is shown'} | ${true} | ${true}
- ${'is not shown'} | ${false} | ${false}
- `('with canUseNewWebIde=$canUseNewWebIde, $desc', async ({ canUseNewWebIde, expectedShow }) => {
- Object.assign(store.state, { canUseNewWebIde });
-
- await nextTick();
-
- expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- show: expectedShow,
- title: __('Switch editors'),
- views: [
- { component: SwitchEditorsView, name: SWITCH_EDITORS_VIEW_NAME, keepAlive: true },
- ],
- }),
- ]),
- );
- });
- });
});
diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js
index 545924c9c11..d82b97561f0 100644
--- a/spec/frontend/ide/components/pipelines/list_spec.js
+++ b/spec/frontend/ide/components/pipelines/list_spec.js
@@ -185,7 +185,7 @@ describe('IDE pipelines list', () => {
},
);
- expect(wrapper.text()).toContain('Found errors in your .gitlab-ci.yml:');
+ expect(wrapper.text()).toContain('Unable to create pipeline');
expect(wrapper.text()).toContain(yamlError);
});
});
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 9921d8cba18..211fee31a9c 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -4,13 +4,17 @@ import { editor as monacoEditor, Range } from 'monaco-editor';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
-import '~/behaviors/markdown/render_gfm';
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 } from '~/editor/constants';
+import {
+ EDITOR_CODE_INSTANCE_FN,
+ EDITOR_DIFF_INSTANCE_FN,
+ EXTENSION_CI_SCHEMA_FILE_NAME_MATCH,
+} 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';
@@ -22,6 +26,9 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer
import SourceEditorInstance from '~/editor/source_editor_instance';
import { file } from '../helpers';
+jest.mock('~/behaviors/markdown/render_gfm');
+jest.mock('~/editor/extensions/source_editor_ci_schema_ext');
+
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
const CURRENT_PROJECT_ID = 'gitlab-org/gitlab';
@@ -46,6 +53,12 @@ const dummyFile = {
tempFile: true,
active: true,
},
+ ciConfig: {
+ ...file(EXTENSION_CI_SCHEMA_FILE_NAME_MATCH),
+ content: '',
+ tempFile: true,
+ active: true,
+ },
empty: {
...file('empty'),
tempFile: false,
@@ -101,6 +114,7 @@ describe('RepoEditor', () => {
let createDiffInstanceSpy;
let createModelSpy;
let applyExtensionSpy;
+ let removeExtensionSpy;
let extensionsStore;
const waitForEditorSetup = () =>
@@ -108,7 +122,7 @@ describe('RepoEditor', () => {
vm.$once('editorSetup', resolve);
});
- const createComponent = async ({ state = {}, activeFile = dummyFile.text } = {}) => {
+ const createComponent = async ({ state = {}, activeFile = dummyFile.text, flags = {} } = {}) => {
const store = prepareStore(state, activeFile);
wrapper = shallowMount(RepoEditor, {
store,
@@ -118,6 +132,9 @@ describe('RepoEditor', () => {
mocks: {
ContentViewer,
},
+ provide: {
+ glFeatures: flags,
+ },
});
await waitForPromises();
vm = wrapper.vm;
@@ -137,6 +154,7 @@ describe('RepoEditor', () => {
createDiffInstanceSpy = jest.spyOn(SourceEditor.prototype, EDITOR_DIFF_INSTANCE_FN);
createModelSpy = jest.spyOn(monacoEditor, 'createModel');
applyExtensionSpy = jest.spyOn(SourceEditorInstance.prototype, 'use');
+ removeExtensionSpy = jest.spyOn(SourceEditorInstance.prototype, 'unuse');
jest.spyOn(service, 'getFileData').mockResolvedValue();
jest.spyOn(service, 'getRawFileData').mockResolvedValue();
});
@@ -177,6 +195,76 @@ describe('RepoEditor', () => {
});
});
+ describe('schema registration for .gitlab-ci.yml', () => {
+ const setup = async (activeFile, flagIsOn = true) => {
+ await createComponent({
+ flags: {
+ schemaLinting: flagIsOn,
+ },
+ });
+ vm.editor.registerCiSchema = jest.fn();
+ if (activeFile) {
+ wrapper.setProps({ file: activeFile });
+ }
+ await waitForPromises();
+ await nextTick();
+ };
+ it.each`
+ flagIsOn | activeFile | shouldUseExtension | desc
+ ${false} | ${dummyFile.markdown} | ${false} | ${`file is not CI config; should NOT`}
+ ${true} | ${dummyFile.markdown} | ${false} | ${`file is not CI config; should NOT`}
+ ${false} | ${dummyFile.ciConfig} | ${false} | ${`file is CI config; should NOT`}
+ ${true} | ${dummyFile.ciConfig} | ${true} | ${`file is CI config; should`}
+ `(
+ 'when the flag is "$flagIsOn", $desc use extension',
+ async ({ flagIsOn, activeFile, shouldUseExtension }) => {
+ await setup(activeFile, flagIsOn);
+
+ if (shouldUseExtension) {
+ expect(applyExtensionSpy).toHaveBeenCalledWith({
+ definition: CiSchemaExtension,
+ });
+ } else {
+ expect(applyExtensionSpy).not.toHaveBeenCalledWith({
+ definition: CiSchemaExtension,
+ });
+ }
+ },
+ );
+ it('stores the fetched extension and does not double-fetch the schema', async () => {
+ await setup();
+ expect(CiSchemaExtension).toHaveBeenCalledTimes(0);
+
+ wrapper.setProps({ file: dummyFile.ciConfig });
+ await waitForPromises();
+ await nextTick();
+ expect(CiSchemaExtension).toHaveBeenCalledTimes(1);
+ expect(vm.CiSchemaExtension).toEqual(CiSchemaExtension);
+ expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(1);
+
+ wrapper.setProps({ file: dummyFile.markdown });
+ await waitForPromises();
+ await nextTick();
+ expect(CiSchemaExtension).toHaveBeenCalledTimes(1);
+ expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(1);
+
+ wrapper.setProps({ file: dummyFile.ciConfig });
+ await waitForPromises();
+ await nextTick();
+ expect(CiSchemaExtension).toHaveBeenCalledTimes(1);
+ expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(2);
+ });
+ it('unuses the existing CI extension if the new model is not CI config', async () => {
+ await setup(dummyFile.ciConfig);
+
+ expect(removeExtensionSpy).not.toHaveBeenCalled();
+ wrapper.setProps({ file: dummyFile.markdown });
+ await waitForPromises();
+ await nextTick();
+ expect(removeExtensionSpy).toHaveBeenCalledWith(CiSchemaExtension);
+ });
+ });
+
describe('when file is markdown', () => {
let mock;
let activeFile;
diff --git a/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js b/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js
deleted file mode 100644
index 7a958391fea..00000000000
--- a/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js
+++ /dev/null
@@ -1,214 +0,0 @@
-import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
-import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import { createAlert } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { logError } from '~/lib/logger';
-import { __ } from '~/locale';
-import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import SwitchEditorsView, {
- MSG_ERROR_ALERT,
- MSG_CONFIRM,
- MSG_TITLE,
- MSG_LEARN_MORE,
- MSG_DESCRIPTION,
-} from '~/ide/components/switch_editors/switch_editors_view.vue';
-import eventHub from '~/ide/eventhub';
-import { createStore } from '~/ide/stores';
-
-jest.mock('~/flash');
-jest.mock('~/lib/logger');
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
-
-const TEST_USER_PREFERENCES_PATH = '/test/user-pref/path';
-const TEST_SWITCH_EDITOR_SVG_PATH = '/test/switch/editor/path.svg';
-const TEST_HREF = '/test/new/web/ide/href';
-
-describe('~/ide/components/switch_editors/switch_editors_view.vue', () => {
- useMockLocationHelper();
-
- let store;
- let wrapper;
- let confirmResolve;
- let requestSpy;
- let skipBeforeunloadSpy;
- let axiosMock;
-
- // region: finders ------------------
- const findButton = () => wrapper.findComponent(GlButton);
- const findEmptyState = () => wrapper.findComponent(GlEmptyState);
-
- // region: actions ------------------
- const triggerSwitchPreference = () => findButton().vm.$emit('click');
- const submitConfirm = async (val) => {
- confirmResolve(val);
-
- // why: We need to wait for promises for the immediate next lines to be executed
- await waitForPromises();
- };
-
- const createComponent = () => {
- wrapper = shallowMount(SwitchEditorsView, {
- store,
- stubs: {
- GlEmptyState,
- },
- });
- };
-
- // region: test setup ------------------
- beforeEach(() => {
- // Setup skip-beforeunload side-effect
- skipBeforeunloadSpy = jest.fn();
- eventHub.$on('skip-beforeunload', skipBeforeunloadSpy);
-
- // Setup request side-effect
- requestSpy = jest.fn().mockImplementation(() => new Promise(() => {}));
- axiosMock = new MockAdapter(axios);
- axiosMock.onPut(TEST_USER_PREFERENCES_PATH).reply(({ data }) => requestSpy(data));
-
- // Setup store
- store = createStore();
- store.state.userPreferencesPath = TEST_USER_PREFERENCES_PATH;
- store.state.switchEditorSvgPath = TEST_SWITCH_EDITOR_SVG_PATH;
- store.state.links = {
- newWebIDEHelpPagePath: TEST_HREF,
- };
-
- // Setup user confirm side-effect
- confirmAction.mockImplementation(
- () =>
- new Promise((resolve) => {
- confirmResolve = resolve;
- }),
- );
- });
-
- afterEach(() => {
- eventHub.$off('skip-beforeunload', skipBeforeunloadSpy);
-
- axiosMock.restore();
- });
-
- // region: tests ------------------
- describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('render empty state', () => {
- expect(findEmptyState().props()).toMatchObject({
- svgPath: TEST_SWITCH_EDITOR_SVG_PATH,
- svgHeight: 150,
- title: MSG_TITLE,
- });
- });
-
- it('render link', () => {
- expect(wrapper.findComponent(GlLink).attributes('href')).toBe(TEST_HREF);
- expect(wrapper.findComponent(GlLink).text()).toBe(MSG_LEARN_MORE);
- });
-
- it('renders description', () => {
- expect(findEmptyState().text()).toContain(MSG_DESCRIPTION);
- });
-
- it('is not loading', () => {
- expect(findButton().props('loading')).toBe(false);
- });
- });
-
- describe('when user triggers switch preference', () => {
- beforeEach(() => {
- createComponent();
-
- triggerSwitchPreference();
- });
-
- it('creates a single confirm', () => {
- // Call again to ensure that we only show 1 confirm action
- triggerSwitchPreference();
-
- expect(confirmAction).toHaveBeenCalledTimes(1);
- expect(confirmAction).toHaveBeenCalledWith(MSG_CONFIRM, {
- primaryBtnText: __('Switch editors'),
- cancelBtnText: __('Cancel'),
- });
- });
-
- it('starts loading', () => {
- expect(findButton().props('loading')).toBe(true);
- });
-
- describe('when user cancels confirm', () => {
- beforeEach(async () => {
- await submitConfirm(false);
- });
-
- it('does not make request', () => {
- expect(requestSpy).not.toHaveBeenCalled();
- });
-
- it('can be triggered again', () => {
- triggerSwitchPreference();
-
- expect(confirmAction).toHaveBeenCalledTimes(2);
- });
- });
-
- describe('when user accepts confirm and response success', () => {
- beforeEach(async () => {
- requestSpy.mockReturnValue([200, {}]);
- await submitConfirm(true);
- });
-
- it('does not handle error', () => {
- expect(logError).not.toHaveBeenCalled();
- expect(createAlert).not.toHaveBeenCalled();
- });
-
- it('emits "skip-beforeunload" and reloads', () => {
- expect(skipBeforeunloadSpy).toHaveBeenCalledTimes(1);
- expect(window.location.reload).toHaveBeenCalledTimes(1);
- });
-
- it('calls request', () => {
- expect(requestSpy).toHaveBeenCalledTimes(1);
- expect(requestSpy).toHaveBeenCalledWith(
- JSON.stringify({ user: { use_legacy_web_ide: false } }),
- );
- });
-
- it('is not loading', () => {
- expect(findButton().props('loading')).toBe(false);
- });
- });
-
- describe('when user accepts confirm and response fails', () => {
- beforeEach(async () => {
- requestSpy.mockReturnValue([400, {}]);
- await submitConfirm(true);
- });
-
- it('handles error', () => {
- expect(logError).toHaveBeenCalledTimes(1);
- expect(logError).toHaveBeenCalledWith(
- 'Error while updating user preferences',
- expect.any(Error),
- );
-
- expect(createAlert).toHaveBeenCalledTimes(1);
- expect(createAlert).toHaveBeenCalledWith({
- message: MSG_ERROR_ALERT,
- });
- });
-
- it('does not reload', () => {
- expect(skipBeforeunloadSpy).not.toHaveBeenCalled();
- expect(window.location.reload).not.toHaveBeenCalled();
- });
- });
- });
-});
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index 067da25cb52..97254ab680b 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -1,62 +1,190 @@
import { start } from '@gitlab/web-ide';
+import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from '~/ide/constants';
import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
+import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
import { TEST_HOST } from 'helpers/test_constants';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('@gitlab/web-ide');
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_action');
+jest.mock('~/lib/utils/create_and_submit_form');
+jest.mock('~/lib/utils/csrf', () => ({
+ token: 'mock-csrf-token',
+ headerKey: 'mock-csrf-header',
+}));
const ROOT_ELEMENT_ID = 'ide';
const TEST_NONCE = 'test123nonce';
const TEST_PROJECT_PATH = 'group1/project1';
const TEST_BRANCH_NAME = '12345-foo-patch';
const TEST_GITLAB_URL = 'https://test-gitlab/';
+const TEST_USER_PREFERENCES_PATH = '/user/preferences';
const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path';
+const TEST_FILE_PATH = 'foo/README.md';
+const TEST_MR_ID = '7';
+const TEST_MR_TARGET_PROJECT = 'gitlab-org/the-real-gitlab';
+const TEST_FORK_INFO = { fork_path: '/forky' };
+const TEST_IDE_REMOTE_PATH = '/-/ide/remote/:remote_host/:remote_path';
+const TEST_START_REMOTE_PARAMS = {
+ remoteHost: 'dev.example.gitlab.com/test',
+ remotePath: '/test/projects/f oo',
+ connectionToken: '123abc',
+};
describe('ide/init_gitlab_web_ide', () => {
+ let resolveConfirm;
+
const createRootElement = () => {
const el = document.createElement('div');
el.id = ROOT_ELEMENT_ID;
// why: We'll test that this class is removed later
- el.classList.add('ide-loading');
+ el.classList.add('test-class');
el.dataset.projectPath = TEST_PROJECT_PATH;
el.dataset.cspNonce = TEST_NONCE;
el.dataset.branchName = TEST_BRANCH_NAME;
+ el.dataset.ideRemotePath = TEST_IDE_REMOTE_PATH;
+ el.dataset.userPreferencesPath = TEST_USER_PREFERENCES_PATH;
+ el.dataset.mergeRequest = TEST_MR_ID;
+ el.dataset.filePath = TEST_FILE_PATH;
document.body.append(el);
};
const findRootElement = () => document.getElementById(ROOT_ELEMENT_ID);
- const act = () => initGitlabWebIDE(findRootElement());
+ const createSubject = () => initGitlabWebIDE(findRootElement());
+ const triggerHandleStartRemote = (startRemoteParams) => {
+ const [, config] = start.mock.calls[0];
+
+ config.handleStartRemote(startRemoteParams);
+ };
beforeEach(() => {
process.env.GITLAB_WEB_IDE_PUBLIC_PATH = TEST_GITLAB_WEB_IDE_PUBLIC_PATH;
window.gon.gitlab_url = TEST_GITLAB_URL;
- createRootElement();
+ confirmAction.mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ resolveConfirm = resolve;
+ }),
+ );
- act();
+ createRootElement();
});
afterEach(() => {
document.body.innerHTML = '';
});
- it('calls start with element', () => {
- expect(start).toHaveBeenCalledWith(findRootElement(), {
- baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
- projectPath: TEST_PROJECT_PATH,
- ref: TEST_BRANCH_NAME,
- gitlabUrl: TEST_GITLAB_URL,
- nonce: TEST_NONCE,
+ describe('default', () => {
+ beforeEach(() => {
+ createSubject();
+ });
+
+ it('calls start with element', () => {
+ expect(start).toHaveBeenCalledTimes(1);
+ expect(start).toHaveBeenCalledWith(findRootElement(), {
+ baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
+ projectPath: TEST_PROJECT_PATH,
+ ref: TEST_BRANCH_NAME,
+ filePath: TEST_FILE_PATH,
+ mrId: TEST_MR_ID,
+ mrTargetProject: '',
+ forkInfo: null,
+ gitlabUrl: TEST_GITLAB_URL,
+ nonce: TEST_NONCE,
+ httpHeaders: {
+ 'mock-csrf-header': 'mock-csrf-token',
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
+ links: {
+ userPreferences: TEST_USER_PREFERENCES_PATH,
+ feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
+ },
+ handleStartRemote: expect.any(Function),
+ });
+ });
+
+ it('clears classes and data from root element', () => {
+ const rootEl = findRootElement();
+
+ // why: Snapshot to test that the element was cleaned including `test-class`
+ expect(rootEl.outerHTML).toBe(
+ '<div id="ide" class="gl--flex-center gl-relative gl-h-full"></div>',
+ );
+ });
+
+ describe('when handleStartRemote is triggered', () => {
+ beforeEach(() => {
+ triggerHandleStartRemote(TEST_START_REMOTE_PARAMS);
+ });
+
+ it('promts for confirm', () => {
+ expect(confirmAction).toHaveBeenCalledWith(expect.any(String), {
+ primaryBtnText: expect.any(String),
+ cancelBtnText: expect.any(String),
+ });
+ });
+
+ it('does not submit, when not confirmed', async () => {
+ resolveConfirm(false);
+
+ await waitForPromises();
+
+ expect(createAndSubmitForm).not.toHaveBeenCalled();
+ });
+
+ it('submits, when confirmed', async () => {
+ resolveConfirm(true);
+
+ await waitForPromises();
+
+ expect(createAndSubmitForm).toHaveBeenCalledWith({
+ url: '/-/ide/remote/dev.example.gitlab.com%2Ftest/test/projects/f%20oo',
+ data: {
+ connection_token: TEST_START_REMOTE_PARAMS.connectionToken,
+ return_url: window.location.href,
+ },
+ });
+ });
+ });
+ });
+
+ describe('when URL has target_project in query params', () => {
+ beforeEach(() => {
+ setWindowLocation(
+ `https://example.com/-/ide?target_project=${encodeURIComponent(TEST_MR_TARGET_PROJECT)}`,
+ );
+
+ createSubject();
+ });
+
+ it('includes mrTargetProject', () => {
+ expect(start).toHaveBeenCalledWith(
+ findRootElement(),
+ expect.objectContaining({
+ mrTargetProject: TEST_MR_TARGET_PROJECT,
+ }),
+ );
});
});
- it('clears classes and data from root element', () => {
- const rootEl = findRootElement();
+ describe('when forkInfo is in dataset', () => {
+ beforeEach(() => {
+ findRootElement().dataset.forkInfo = JSON.stringify(TEST_FORK_INFO);
- // why: Snapshot to test that `ide-loading` was removed and no other
- // artifacts are remaining.
- expect(rootEl.outerHTML).toBe(
- '<div id="ide" class="gl--flex-center gl-relative gl-h-full"></div>',
- );
+ createSubject();
+ });
+
+ it('includes forkInfo', () => {
+ expect(start).toHaveBeenCalledWith(
+ findRootElement(),
+ expect.objectContaining({
+ forkInfo: TEST_FORK_INFO,
+ }),
+ );
+ });
});
});
diff --git a/spec/frontend/ide/lib/common/model_spec.js b/spec/frontend/ide/lib/common/model_spec.js
index 5d1623429c0..39c50f628c2 100644
--- a/spec/frontend/ide/lib/common/model_spec.js
+++ b/spec/frontend/ide/lib/common/model_spec.js
@@ -149,7 +149,6 @@ describe('Multi-file editor library model', () => {
model.updateOptions({ insertSpaces: true, someOption: 'some value' });
expect(model.options).toEqual({
- endOfLine: 0,
insertFinalNewline: true,
insertSpaces: true,
someOption: 'some value',
@@ -181,16 +180,12 @@ describe('Multi-file editor library model', () => {
describe('applyCustomOptions', () => {
it.each`
option | value | contentBefore | contentAfter
- ${'endOfLine'} | ${0} | ${'hello\nworld\n'} | ${'hello\nworld\n'}
- ${'endOfLine'} | ${0} | ${'hello\r\nworld\r\n'} | ${'hello\nworld\n'}
- ${'endOfLine'} | ${1} | ${'hello\nworld\n'} | ${'hello\r\nworld\r\n'}
- ${'endOfLine'} | ${1} | ${'hello\r\nworld\r\n'} | ${'hello\r\nworld\r\n'}
${'insertFinalNewline'} | ${true} | ${'hello\nworld'} | ${'hello\nworld\n'}
${'insertFinalNewline'} | ${true} | ${'hello\nworld\n'} | ${'hello\nworld\n'}
${'insertFinalNewline'} | ${false} | ${'hello\nworld'} | ${'hello\nworld'}
${'trimTrailingWhitespace'} | ${true} | ${'hello \t\nworld \t\n'} | ${'hello\nworld\n'}
- ${'trimTrailingWhitespace'} | ${true} | ${'hello \t\r\nworld \t\r\n'} | ${'hello\nworld\n'}
- ${'trimTrailingWhitespace'} | ${false} | ${'hello \t\r\nworld \t\r\n'} | ${'hello \t\nworld \t\n'}
+ ${'trimTrailingWhitespace'} | ${true} | ${'hello \t\r\nworld \t\r\n'} | ${'hello\r\nworld\r\n'}
+ ${'trimTrailingWhitespace'} | ${false} | ${'hello \t\r\nworld \t\r\n'} | ${'hello \t\r\nworld \t\r\n'}
`(
'correctly applies custom option $option=$value to content',
({ option, value, contentBefore, contentAfter }) => {
diff --git a/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
new file mode 100644
index 00000000000..4b4e96f3b41
--- /dev/null
+++ b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
@@ -0,0 +1,22 @@
+import { getBaseConfig } from '~/ide/lib/gitlab_web_ide/get_base_config';
+import { TEST_HOST } from 'helpers/test_constants';
+
+const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/gitlab-web-ide/public/path';
+const TEST_GITLAB_URL = 'https://gdk.test/';
+
+describe('~/ide/lib/gitlab_web_ide/get_base_config', () => {
+ it('returns base properties for @gitlab/web-ide config', () => {
+ // why: add trailing "/" to test that it gets removed
+ process.env.GITLAB_WEB_IDE_PUBLIC_PATH = `${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}/`;
+ window.gon.gitlab_url = TEST_GITLAB_URL;
+
+ // act
+ const actual = getBaseConfig();
+
+ // asset
+ expect(actual).toEqual({
+ baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
+ gitlabUrl: TEST_GITLAB_URL,
+ });
+ });
+});
diff --git a/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js
new file mode 100644
index 00000000000..35cf41b31f5
--- /dev/null
+++ b/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js
@@ -0,0 +1,32 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setupRootElement } from '~/ide/lib/gitlab_web_ide/setup_root_element';
+
+describe('~/ide/lib/gitlab_web_ide/setup_root_element', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <div id="ide-test-root" class="js-not-a-real-class">
+ <span>We are loading lots of stuff...</span>
+ </div>
+ `);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ const findIDERoot = () => document.getElementById('ide-test-root');
+
+ it('has no children, has original ID, and classes', () => {
+ const result = setupRootElement(findIDERoot());
+
+ // why: Assert that the return element matches the new one found in the dom
+ // (implying a el.replaceWith...)
+ expect(result).toBe(findIDERoot());
+ expect(result).toMatchInlineSnapshot(`
+ <div
+ class="gl--flex-center gl-relative gl-h-full"
+ id="ide-test-root"
+ />
+ `);
+ });
+});
diff --git a/spec/frontend/ide/remote/index_spec.js b/spec/frontend/ide/remote/index_spec.js
new file mode 100644
index 00000000000..0f23b0a4e45
--- /dev/null
+++ b/spec/frontend/ide/remote/index_spec.js
@@ -0,0 +1,91 @@
+import { startRemote } from '@gitlab/web-ide';
+import { getBaseConfig, setupRootElement } from '~/ide/lib/gitlab_web_ide';
+import { mountRemoteIDE } from '~/ide/remote';
+import { TEST_HOST } from 'helpers/test_constants';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+
+jest.mock('@gitlab/web-ide');
+jest.mock('~/ide/lib/gitlab_web_ide');
+
+const TEST_DATA = {
+ remoteHost: 'example.com:3443',
+ remotePath: 'test/path/gitlab',
+ cspNonce: 'just7some8noncense',
+ connectionToken: 'connectAtoken',
+ returnUrl: 'https://example.com/return',
+};
+
+const TEST_BASE_CONFIG = {
+ gitlabUrl: '/test/gitlab',
+};
+
+const TEST_RETURN_URL_SAME_ORIGIN = `${TEST_HOST}/foo/example`;
+
+describe('~/ide/remote/index', () => {
+ useMockLocationHelper();
+ const originalHref = window.location.href;
+
+ let el;
+ let rootEl;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ Object.entries(TEST_DATA).forEach(([key, value]) => {
+ el.dataset[key] = value;
+ });
+
+ // Stub setupRootElement so we can assert on return element
+ rootEl = document.createElement('div');
+ setupRootElement.mockReturnValue(rootEl);
+
+ // Stub getBaseConfig so we can assert
+ getBaseConfig.mockReturnValue(TEST_BASE_CONFIG);
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ mountRemoteIDE(el);
+ });
+
+ it('calls startRemote', () => {
+ expect(startRemote).toHaveBeenCalledWith(rootEl, {
+ ...TEST_BASE_CONFIG,
+ nonce: TEST_DATA.cspNonce,
+ connectionToken: TEST_DATA.connectionToken,
+ remoteAuthority: `/${TEST_DATA.remoteHost}`,
+ hostPath: `/${TEST_DATA.remotePath}`,
+ handleError: expect.any(Function),
+ handleClose: expect.any(Function),
+ });
+ });
+ });
+
+ describe.each`
+ returnUrl | fnName | reloadExpectation | hrefExpectation
+ ${TEST_DATA.returnUrl} | ${'handleError'} | ${1} | ${originalHref}
+ ${TEST_DATA.returnUrl} | ${'handleClose'} | ${1} | ${originalHref}
+ ${TEST_RETURN_URL_SAME_ORIGIN} | ${'handleClose'} | ${0} | ${TEST_RETURN_URL_SAME_ORIGIN}
+ ${TEST_RETURN_URL_SAME_ORIGIN} | ${'handleError'} | ${0} | ${TEST_RETURN_URL_SAME_ORIGIN}
+ ${''} | ${'handleClose'} | ${1} | ${originalHref}
+ `(
+ 'with returnUrl=$returnUrl and fn=$fnName',
+ ({ returnUrl, fnName, reloadExpectation, hrefExpectation }) => {
+ beforeEach(() => {
+ el.dataset.returnUrl = returnUrl;
+
+ mountRemoteIDE(el);
+ });
+
+ it('changes location', () => {
+ expect(window.location.reload).not.toHaveBeenCalled();
+
+ const [, config] = startRemote.mock.calls[0];
+
+ config[fnName]();
+
+ expect(window.location.reload).toHaveBeenCalledTimes(reloadExpectation);
+ expect(window.location.href).toBe(hrefExpectation);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 0fab828dfb3..5847e8e1518 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -6,7 +6,7 @@ import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.
import services from '~/ide/services';
import { query, mutate } from '~/ide/services/gql';
import { escapeFileUrl } from '~/lib/utils/url_utility';
-import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
+import ciConfig from '~/ci/pipeline_editor/graphql/queries/ci_config.query.graphql';
import { projectData } from '../mock_data';
jest.mock('~/api');
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
index fc00bd075e7..8d21088bcaf 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
@@ -10,7 +10,7 @@ import {
import * as messages from '~/ide/stores/modules/terminal/messages';
import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
const TEST_PROJECT_PATH = 'lorem/root';
const TEST_BRANCH_ID = 'main';
@@ -78,7 +78,7 @@ describe('IDE store terminal check actions', () => {
describe('receiveConfigCheckError', () => {
it('handles error response', () => {
- const status = httpStatus.UNPROCESSABLE_ENTITY;
+ const status = HTTP_STATUS_UNPROCESSABLE_ENTITY;
const payload = { response: { status } };
return testAction(
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
index f48797415df..df365442c67 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
@@ -6,7 +6,7 @@ import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/termi
import * as messages from '~/ide/stores/modules/terminal/messages';
import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -285,7 +285,7 @@ describe('IDE store terminal session controls actions', () => {
);
});
- [httpStatus.NOT_FOUND, httpStatus.UNPROCESSABLE_ENTITY].forEach((status) => {
+ [httpStatus.NOT_FOUND, HTTP_STATUS_UNPROCESSABLE_ENTITY].forEach((status) => {
it(`dispatches request and startSession on ${status}`, () => {
mock
.onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' })
diff --git a/spec/frontend/ide/stores/modules/terminal/messages_spec.js b/spec/frontend/ide/stores/modules/terminal/messages_spec.js
index e8f375a70b5..2a802d6b4af 100644
--- a/spec/frontend/ide/stores/modules/terminal/messages_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/messages_spec.js
@@ -1,7 +1,7 @@
import { escape } from 'lodash';
import { TEST_HOST } from 'spec/test_constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import { sprintf } from '~/locale';
const TEST_HELP_URL = `${TEST_HOST}/help`;
@@ -9,7 +9,7 @@ const TEST_HELP_URL = `${TEST_HOST}/help`;
describe('IDE store terminal messages', () => {
describe('configCheckError', () => {
it('returns job error, with status UNPROCESSABLE_ENTITY', () => {
- const result = messages.configCheckError(httpStatus.UNPROCESSABLE_ENTITY, TEST_HELP_URL);
+ const result = messages.configCheckError(HTTP_STATUS_UNPROCESSABLE_ENTITY, TEST_HELP_URL);
expect(result).toBe(
sprintf(
diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js
index b896437ecb2..31e097cfa7b 100644
--- a/spec/frontend/import_entities/components/group_dropdown_spec.js
+++ b/spec/frontend/import_entities/components/group_dropdown_spec.js
@@ -1,16 +1,61 @@
import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import GroupDropdown from '~/import_entities/components/group_dropdown.vue';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+
+Vue.use(VueApollo);
+
+const makeGroupMock = (fullPath) => ({
+ id: `gid://gitlab/Group/${fullPath}`,
+ fullPath,
+ name: fullPath,
+ visibility: 'public',
+ webUrl: `http://gdk.test:3000/groups/${fullPath}`,
+ __typename: 'Group',
+});
+
+const AVAILABLE_NAMESPACES = [
+ makeGroupMock('match1'),
+ makeGroupMock('unrelated'),
+ makeGroupMock('match2'),
+];
+
+const SEARCH_NAMESPACES_MOCK = Promise.resolve({
+ data: {
+ currentUser: {
+ id: 'gid://gitlab/User/1',
+ groups: {
+ nodes: AVAILABLE_NAMESPACES,
+ __typename: 'GroupConnection',
+ },
+ namespace: {
+ id: 'gid://gitlab/Namespaces::UserNamespace/1',
+ fullPath: 'root',
+ __typename: 'Namespace',
+ },
+ __typename: 'UserCore',
+ },
+ },
+});
describe('Import entities group dropdown component', () => {
let wrapper;
let namespacesTracker;
const createComponent = (propsData) => {
+ const apolloProvider = createMockApollo([
+ [searchNamespacesWhereUserCanCreateProjectsQuery, () => SEARCH_NAMESPACES_MOCK],
+ ]);
+
namespacesTracker = jest.fn();
wrapper = shallowMount(GroupDropdown, {
+ apolloProvider,
scopedSlots: {
default: namespacesTracker,
},
@@ -23,33 +68,30 @@ describe('Import entities group dropdown component', () => {
wrapper.destroy();
});
- it('passes namespaces from props to default slot', () => {
- const namespaces = [
- { id: 1, fullPath: 'ns1' },
- { id: 2, fullPath: 'ns2' },
- ];
- createComponent({ namespaces });
+ it('passes namespaces from graphql query to default slot', async () => {
+ createComponent();
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await nextTick();
+ await waitForPromises();
+ await nextTick();
- expect(namespacesTracker).toHaveBeenCalledWith({ namespaces });
+ expect(namespacesTracker).toHaveBeenCalledWith({ namespaces: AVAILABLE_NAMESPACES });
});
it('filters namespaces based on user input', async () => {
- const namespaces = [
- { id: 1, fullPath: 'match1' },
- { id: 2, fullPath: 'some unrelated' },
- { id: 3, fullPath: 'match2' },
- ];
- createComponent({ namespaces });
+ createComponent();
namespacesTracker.mockReset();
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'match');
-
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await nextTick();
+ await waitForPromises();
await nextTick();
expect(namespacesTracker).toHaveBeenCalledWith({
namespaces: [
- { id: 1, fullPath: 'match1' },
- { id: 3, fullPath: 'match2' },
+ expect.objectContaining({ fullPath: 'match1' }),
+ expect.objectContaining({ fullPath: 'match2' }),
],
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index 61f860688dc..f7a97f22d44 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -15,8 +15,13 @@ import ImportTable from '~/import_entities/import_groups/components/import_table
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
-import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures';
+import {
+ AVAILABLE_NAMESPACES,
+ availableNamespacesFixture,
+ generateFakeEntry,
+} from '../graphql/fixtures';
jest.mock('~/flash');
jest.mock('~/import_entities/import_groups/services/status_poller');
@@ -60,15 +65,22 @@ describe('import table', () => {
wrapper.findAll('tbody td input[type=checkbox]').at(idx).setChecked(true);
const createComponent = ({ bulkImportSourceGroups, importGroups, defaultTargetNamespace }) => {
- apolloProvider = createMockApollo([], {
- Query: {
- availableNamespaces: () => availableNamespacesFixture,
- bulkImportSourceGroups,
- },
- Mutation: {
- importGroups,
+ apolloProvider = createMockApollo(
+ [
+ [
+ searchNamespacesWhereUserCanCreateProjectsQuery,
+ () => Promise.resolve(availableNamespacesFixture),
+ ],
+ ],
+ {
+ Query: {
+ bulkImportSourceGroups,
+ },
+ Mutation: {
+ importGroups,
+ },
},
- });
+ );
wrapper = mount(ImportTable, {
propsData: {
@@ -173,7 +185,7 @@ describe('import table', () => {
});
it('respects default namespace if provided', async () => {
- const targetNamespace = availableNamespacesFixture[1];
+ const targetNamespace = AVAILABLE_NAMESPACES[1];
createComponent({
bulkImportSourceGroups: () => ({
@@ -227,7 +239,7 @@ describe('import table', () => {
{
newName: FAKE_GROUP.lastImportTarget.newName,
sourceGroupId: FAKE_GROUP.id,
- targetNamespace: availableNamespacesFixture[0].fullPath,
+ targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
},
],
},
@@ -519,12 +531,12 @@ describe('import table', () => {
variables: {
importRequests: [
{
- targetNamespace: availableNamespacesFixture[0].fullPath,
+ targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
newName: NEW_GROUPS[0].lastImportTarget.newName,
sourceGroupId: NEW_GROUPS[0].id,
},
{
- targetNamespace: availableNamespacesFixture[0].fullPath,
+ targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
newName: NEW_GROUPS[1].lastImportTarget.newName,
sourceGroupId: NEW_GROUPS[1].id,
},
diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
index 18dc1217fec..d5286e71c44 100644
--- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
@@ -1,9 +1,22 @@
import { GlDropdownItem, GlFormInput } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
import { STATUSES } from '~/import_entities/constants';
import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue';
-import { generateFakeEntry, availableNamespacesFixture } from '../graphql/fixtures';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+
+import {
+ generateFakeEntry,
+ availableNamespacesFixture,
+ AVAILABLE_NAMESPACES,
+} from '../graphql/fixtures';
+
+Vue.use(VueApollo);
const generateFakeTableEntry = ({ flags = {}, ...config }) => {
const entry = generateFakeEntry(config);
@@ -11,7 +24,7 @@ const generateFakeTableEntry = ({ flags = {}, ...config }) => {
return {
...entry,
importTarget: {
- targetNamespace: availableNamespacesFixture[0],
+ targetNamespace: AVAILABLE_NAMESPACES[0],
newName: entry.lastImportTarget.newName,
},
flags,
@@ -20,16 +33,24 @@ const generateFakeTableEntry = ({ flags = {}, ...config }) => {
describe('import target cell', () => {
let wrapper;
+ let apolloProvider;
let group;
const findNameInput = () => wrapper.findComponent(GlFormInput);
const findNamespaceDropdown = () => wrapper.findComponent(ImportGroupDropdown);
const createComponent = (props) => {
+ apolloProvider = createMockApollo([
+ [
+ searchNamespacesWhereUserCanCreateProjectsQuery,
+ () => Promise.resolve(availableNamespacesFixture),
+ ],
+ ]);
+
wrapper = shallowMount(ImportTargetCell, {
+ apolloProvider,
stubs: { ImportGroupDropdown },
propsData: {
- availableNamespaces: availableNamespacesFixture,
groupPathRegex: /.*/,
...props,
},
@@ -42,9 +63,12 @@ describe('import target cell', () => {
});
describe('events', () => {
- beforeEach(() => {
+ beforeEach(async () => {
group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE });
createComponent({ group });
+ await nextTick();
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await nextTick();
});
it('emits update-new-name when input value is changed', () => {
@@ -59,7 +83,9 @@ describe('import target cell', () => {
dropdownItem.vm.$emit('click');
expect(wrapper.emitted('update-target-namespace')).toBeDefined();
- expect(wrapper.emitted('update-target-namespace')[0][0]).toBe(availableNamespacesFixture[1]);
+ expect(wrapper.emitted('update-target-namespace')[0][0]).toStrictEqual(
+ AVAILABLE_NAMESPACES[1],
+ );
});
});
@@ -94,18 +120,20 @@ describe('import target cell', () => {
expect(items).toHaveLength(1);
});
- it('renders both no parent option and available namespaces list when available namespaces list is not empty', () => {
+ it('renders both no parent option and available namespaces list when available namespaces list is not empty', async () => {
createComponent({
group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }),
- availableNamespaces: availableNamespacesFixture,
});
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await waitForPromises();
+ await nextTick();
const [firstItem, ...rest] = findNamespaceDropdown()
.findAllComponents(GlDropdownItem)
.wrappers.map((w) => w.text());
expect(firstItem).toBe('No parent');
- expect(rest).toHaveLength(availableNamespacesFixture.length);
+ expect(rest).toHaveLength(AVAILABLE_NAMESPACES.length);
});
describe('when entity is not available for import', () => {
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index 52c868e5356..adc4ebcffb8 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -10,12 +10,11 @@ import {
import { LocalStorageCache } from '~/import_entities/import_groups/graphql/services/local_storage_cache';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import updateImportStatusMutation from '~/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql';
-import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
-import { statusEndpointFixture, availableNamespacesFixture } from './fixtures';
+import { statusEndpointFixture } from './fixtures';
jest.mock('~/flash');
jest.mock('~/import_entities/import_groups/graphql/services/local_storage_cache', () => ({
@@ -28,7 +27,6 @@ jest.mock('~/import_entities/import_groups/graphql/services/local_storage_cache'
const FAKE_ENDPOINTS = {
status: '/fake_status_url',
- availableNamespaces: '/fake_available_namespaces',
createBulkImport: '/fake_create_bulk_import',
jobs: '/fake_jobs',
};
@@ -55,14 +53,6 @@ describe('Bulk import resolvers', () => {
client = createClient();
axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
- axiosMockAdapter.onGet(FAKE_ENDPOINTS.availableNamespaces).reply(
- httpStatus.OK,
- availableNamespacesFixture.map((ns) => ({
- id: ns.id,
- full_path: ns.fullPath,
- })),
- );
-
client.watchQuery({ query: bulkImportSourceGroupsQuery }).subscribe(({ data }) => {
results = data.bulkImportSourceGroups.nodes;
});
@@ -75,22 +65,6 @@ describe('Bulk import resolvers', () => {
});
describe('queries', () => {
- describe('availableNamespaces', () => {
- let namespacesResults;
- beforeEach(async () => {
- const response = await client.query({ query: availableNamespacesQuery });
- namespacesResults = response.data.availableNamespaces;
- });
-
- it('mirrors REST endpoint response fields', () => {
- const extractRelevantFields = (obj) => ({ id: obj.id, full_path: obj.full_path });
-
- expect(namespacesResults.map(extractRelevantFields)).toStrictEqual(
- availableNamespacesFixture.map(extractRelevantFields),
- );
- });
- });
-
describe('bulkImportSourceGroups', () => {
it('respects cached import state when provided by group manager', async () => {
const [localStorageCache] = LocalStorageCache.mock.instances;
diff --git a/spec/frontend/import_entities/import_groups/graphql/fixtures.js b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
index 938020e03f0..7530e9fc348 100644
--- a/spec/frontend/import_entities/import_groups/graphql/fixtures.js
+++ b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
@@ -59,9 +59,36 @@ export const statusEndpointFixture = {
},
};
-export const availableNamespacesFixture = Object.freeze([
- { id: 24, fullPath: 'Commit451' },
- { id: 22, fullPath: 'gitlab-org' },
- { id: 23, fullPath: 'gnuwget' },
- { id: 25, fullPath: 'jashkenas' },
-]);
+const makeGroupMock = ({ id, fullPath }) => ({
+ id,
+ fullPath,
+ name: fullPath,
+ visibility: 'public',
+ webUrl: `http://gdk.test:3000/groups/${fullPath}`,
+ __typename: 'Group',
+});
+
+export const AVAILABLE_NAMESPACES = [
+ makeGroupMock({ id: 24, fullPath: 'Commit451' }),
+ makeGroupMock({ id: 22, fullPath: 'gitlab-org' }),
+ makeGroupMock({ id: 23, fullPath: 'gnuwget' }),
+ makeGroupMock({ id: 25, fullPath: 'jashkenas' }),
+];
+
+export const availableNamespacesFixture = {
+ data: {
+ currentUser: {
+ id: 'gid://gitlab/User/1',
+ groups: {
+ nodes: AVAILABLE_NAMESPACES,
+ __typename: 'GroupConnection',
+ },
+ namespace: {
+ id: 'gid://gitlab/Namespaces::UserNamespace/1',
+ fullPath: 'root',
+ __typename: 'Namespace',
+ },
+ __typename: 'UserCore',
+ },
+ },
+};
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
index 53807167fe8..51f82dab381 100644
--- a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
@@ -59,7 +59,6 @@ describe('ImportProjectsTable', () => {
actions: {
fetchRepos: fetchReposFn,
fetchJobs: jest.fn(),
- fetchNamespaces: jest.fn(),
importAll: importAllFn,
stopJobsPolling: jest.fn(),
clearJobsEtagPoll: jest.fn(),
@@ -95,12 +94,6 @@ describe('ImportProjectsTable', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
- it('renders a loading icon while namespaces are loading', () => {
- createComponent({ state: { isLoadingNamespaces: true } });
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
-
it('renders a table with provider repos', () => {
const repositories = [
{ importSource: { id: 1 }, importedProject: null },
@@ -214,35 +207,52 @@ describe('ImportProjectsTable', () => {
});
describe('when paginatable is set to true', () => {
- const pageInfo = { page: 1 };
+ const initState = {
+ namespaces: [{ fullPath: 'path' }],
+ pageInfo: { page: 1, hasNextPage: true },
+ repositories: [
+ { importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
+ ],
+ };
+
+ describe('with hasNextPage true', () => {
+ beforeEach(() => {
+ createComponent({
+ state: initState,
+ paginatable: true,
+ });
+ });
- beforeEach(() => {
- createComponent({
- state: {
- namespaces: [{ fullPath: 'path' }],
- pageInfo,
- repositories: [
- { importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
- ],
- },
- paginatable: true,
+ it('does not call fetchRepos on mount', () => {
+ expect(fetchReposFn).not.toHaveBeenCalled();
});
- });
- it('does not call fetchRepos on mount', () => {
- expect(fetchReposFn).not.toHaveBeenCalled();
- });
+ it('renders intersection observer component', () => {
+ expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true);
+ });
+
+ it('calls fetchRepos when intersection observer appears', async () => {
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
- it('renders intersection observer component', () => {
- expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true);
+ await nextTick();
+
+ expect(fetchReposFn).toHaveBeenCalled();
+ });
});
- it('calls fetchRepos when intersection observer appears', async () => {
- wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
+ describe('with hasNextPage false', () => {
+ beforeEach(() => {
+ initState.pageInfo.hasNextPage = false;
- await nextTick();
+ createComponent({
+ state: initState,
+ paginatable: true,
+ });
+ });
- expect(fetchReposFn).toHaveBeenCalled();
+ it('does not render intersection observer component', () => {
+ expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
index 40934e90b78..d686036781f 100644
--- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -10,13 +10,13 @@ import ProviderRepoTableRow from '~/import_entities/import_projects/components/p
describe('ProviderRepoTableRow', () => {
let wrapper;
const fetchImport = jest.fn();
+ const cancelImport = jest.fn();
const setImportTarget = jest.fn();
const fakeImportTarget = {
targetNamespace: 'target',
newName: 'newName',
};
- const availableNamespaces = ['test'];
const userNamespace = 'root';
function initStore(initialState) {
@@ -25,7 +25,7 @@ describe('ProviderRepoTableRow', () => {
getters: {
getImportTarget: () => () => fakeImportTarget,
},
- actions: { fetchImport, setImportTarget },
+ actions: { fetchImport, cancelImport, setImportTarget },
});
return store;
@@ -37,6 +37,14 @@ describe('ProviderRepoTableRow', () => {
return buttons.length ? buttons.at(0) : buttons;
};
+ const findCancelButton = () => {
+ const buttons = wrapper
+ .findAllComponents(GlButton)
+ .filter((node) => node.attributes('aria-label') === 'Cancel');
+
+ return buttons.length ? buttons.at(0) : buttons;
+ };
+
function mountComponent(props) {
Vue.use(Vuex);
@@ -44,7 +52,7 @@ describe('ProviderRepoTableRow', () => {
wrapper = shallowMount(ProviderRepoTableRow, {
store,
- propsData: { availableNamespaces, userNamespace, optionalStages: {}, ...props },
+ propsData: { userNamespace, optionalStages: {}, ...props },
});
}
@@ -78,9 +86,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders a group namespace select', () => {
- expect(wrapper.findComponent(ImportGroupDropdown).props().namespaces).toBe(
- availableNamespaces,
- );
+ expect(wrapper.findComponent(ImportGroupDropdown).exists()).toBe(true);
});
it('renders import button', () => {
@@ -113,6 +119,52 @@ describe('ProviderRepoTableRow', () => {
});
});
+ describe('when rendering importing project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ },
+ importedProject: {
+ id: 1,
+ fullPath: 'fullPath',
+ importSource: 'importSource',
+ importStatus: STATUSES.STARTED,
+ },
+ };
+
+ describe('when cancelable is true', () => {
+ beforeEach(() => {
+ mountComponent({ repo, cancelable: true });
+ });
+
+ it('shows cancel button', () => {
+ expect(findCancelButton().isVisible()).toBe(true);
+ });
+
+ it('cancels import when clicking cancel button', async () => {
+ findCancelButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(cancelImport).toHaveBeenCalledWith(expect.anything(), {
+ repoId: repo.importSource.id,
+ });
+ });
+ });
+
+ describe('when cancelable is false', () => {
+ beforeEach(() => {
+ mountComponent({ repo, cancelable: false });
+ });
+
+ it('hides cancel button', () => {
+ expect(findCancelButton().isVisible()).toBe(false);
+ });
+ });
+ });
+
describe('when rendering imported project', () => {
const FAKE_STATS = {};
diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js
index e154863f339..4b34c21daa3 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
-import { STATUSES } from '~/import_entities/constants';
+import { STATUSES, PROVIDERS } from '~/import_entities/constants';
import actionsFactory from '~/import_entities/import_projects/store/actions';
import { getImportTarget } from '~/import_entities/import_projects/store/getters';
import {
@@ -13,11 +13,10 @@ import {
RECEIVE_IMPORT_SUCCESS,
RECEIVE_IMPORT_ERROR,
RECEIVE_JOBS_SUCCESS,
- REQUEST_NAMESPACES,
- RECEIVE_NAMESPACES_SUCCESS,
- RECEIVE_NAMESPACES_ERROR,
+ CANCEL_IMPORT_SUCCESS,
SET_PAGE,
SET_FILTER,
+ SET_PAGE_CURSORS,
} from '~/import_entities/import_projects/store/mutation_types';
import state from '~/import_entities/import_projects/store/state';
import axios from '~/lib/utils/axios_utils';
@@ -30,7 +29,7 @@ const endpoints = {
reposPath: MOCK_ENDPOINT,
importPath: MOCK_ENDPOINT,
jobsPath: MOCK_ENDPOINT,
- namespacesPath: MOCK_ENDPOINT,
+ cancelPath: MOCK_ENDPOINT,
};
const {
@@ -39,8 +38,8 @@ const {
importAll,
fetchRepos,
fetchImport,
+ cancelImport,
fetchJobs,
- fetchNamespaces,
setFilter,
} = actionsFactory({
endpoints,
@@ -59,14 +58,17 @@ describe('import_projects store actions', () => {
...state(),
defaultTargetNamespace,
repositories: [
- { importSource: { id: importRepoId, sanitizedName }, importStatus: STATUSES.NONE },
+ {
+ importSource: { id: importRepoId, sanitizedName },
+ importedProject: { importStatus: STATUSES.NONE },
+ },
{
importSource: { id: otherImportRepoId, sanitizedName: 's2' },
- importStatus: STATUSES.NONE,
+ importedProject: { importStatus: STATUSES.NONE },
},
{
importSource: { id: 3, sanitizedName: 's3', incompatible: true },
- importStatus: STATUSES.NONE,
+ importedProject: { importStatus: STATUSES.NONE },
},
],
provider: 'provider',
@@ -77,7 +79,11 @@ describe('import_projects store actions', () => {
describe('fetchRepos', () => {
let mock;
- const payload = { imported_projects: [{}], provider_repos: [{}] };
+ const payload = {
+ imported_projects: [{}],
+ provider_repos: [{}],
+ page_info: { startCursor: 'start', endCursor: 'end', hasNextPage: true },
+ };
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -85,23 +91,53 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
- it('commits REQUEST_REPOS, SET_PAGE, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
- mock.onGet(MOCK_ENDPOINT).reply(200, payload);
+ describe('with a successful request', () => {
+ it('commits REQUEST_REPOS, SET_PAGE, RECEIVE_REPOS_SUCCESS mutations', () => {
+ mock.onGet(MOCK_ENDPOINT).reply(200, payload);
- return testAction(
- fetchRepos,
- null,
- localState,
- [
- { type: REQUEST_REPOS },
- { type: SET_PAGE, payload: 1 },
- {
- type: RECEIVE_REPOS_SUCCESS,
- payload: convertObjectPropsToCamelCase(payload, { deep: true }),
- },
- ],
- [],
- );
+ return testAction(
+ fetchRepos,
+ null,
+ localState,
+ [
+ { type: REQUEST_REPOS },
+ { type: SET_PAGE, payload: 1 },
+ {
+ type: RECEIVE_REPOS_SUCCESS,
+ payload: convertObjectPropsToCamelCase(payload, { deep: true }),
+ },
+ ],
+ [],
+ );
+ });
+
+ describe('when provider is GITHUB_PROVIDER', () => {
+ beforeEach(() => {
+ localState.provider = PROVIDERS.GITHUB;
+ });
+
+ it('commits SET_PAGE_CURSORS instead of SET_PAGE', () => {
+ mock.onGet(MOCK_ENDPOINT).reply(200, payload);
+
+ return testAction(
+ fetchRepos,
+ null,
+ localState,
+ [
+ { type: REQUEST_REPOS },
+ {
+ type: SET_PAGE_CURSORS,
+ payload: { startCursor: 'start', endCursor: 'end', hasNextPage: true },
+ },
+ {
+ type: RECEIVE_REPOS_SUCCESS,
+ payload: convertObjectPropsToCamelCase(payload, { deep: true }),
+ },
+ ],
+ [],
+ );
+ });
+ });
});
it('commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {
@@ -116,18 +152,52 @@ describe('import_projects store actions', () => {
);
});
- it('includes page in url query params', async () => {
- let requestedUrl;
- mock.onGet().reply((config) => {
- requestedUrl = config.url;
- return [200, payload];
+ describe('with pagination params', () => {
+ it('includes page in url query params', async () => {
+ let requestedUrl;
+ mock.onGet().reply((config) => {
+ requestedUrl = config.url;
+ return [200, payload];
+ });
+
+ const localStateWithPage = { ...localState, pageInfo: { page: 2 } };
+
+ await testAction(
+ fetchRepos,
+ null,
+ localStateWithPage,
+ expect.any(Array),
+ expect.any(Array),
+ );
+
+ expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?page=${localStateWithPage.pageInfo.page + 1}`);
});
- const localStateWithPage = { ...localState, pageInfo: { page: 2 } };
+ describe('when provider is "github"', () => {
+ beforeEach(() => {
+ localState.provider = PROVIDERS.GITHUB;
+ });
+
+ it('includes cursor in url query params', async () => {
+ let requestedUrl;
+ mock.onGet().reply((config) => {
+ requestedUrl = config.url;
+ return [200, payload];
+ });
- await testAction(fetchRepos, null, localStateWithPage, expect.any(Array), expect.any(Array));
+ const localStateWithPage = { ...localState, pageInfo: { endCursor: 'endTest' } };
- expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?page=${localStateWithPage.pageInfo.page + 1}`);
+ await testAction(
+ fetchRepos,
+ null,
+ localStateWithPage,
+ expect.any(Array),
+ expect.any(Array),
+ );
+
+ expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?after=endTest`);
+ });
+ });
});
it('correctly keeps current page on an unsuccessful request', () => {
@@ -319,51 +389,6 @@ describe('import_projects store actions', () => {
});
});
- describe('fetchNamespaces', () => {
- let mock;
- const namespaces = [{ full_name: 'test/ns1' }, { full_name: 'test_ns2' }];
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => mock.restore());
-
- it('commits REQUEST_NAMESPACES and RECEIVE_NAMESPACES_SUCCESS on success', async () => {
- mock.onGet(MOCK_ENDPOINT).reply(200, namespaces);
-
- await testAction(
- fetchNamespaces,
- null,
- localState,
- [
- { type: REQUEST_NAMESPACES },
- {
- type: RECEIVE_NAMESPACES_SUCCESS,
- payload: convertObjectPropsToCamelCase(namespaces, { deep: true }),
- },
- ],
- [],
- );
- });
-
- it('commits REQUEST_NAMESPACES and RECEIVE_NAMESPACES_ERROR and shows generic error message on an unsuccessful request', async () => {
- mock.onGet(MOCK_ENDPOINT).reply(500);
-
- await testAction(
- fetchNamespaces,
- null,
- localState,
- [{ type: REQUEST_NAMESPACES }, { type: RECEIVE_NAMESPACES_ERROR }],
- [],
- );
-
- expect(createAlert).toHaveBeenCalledWith({
- message: 'Requesting namespaces failed',
- });
- });
- });
-
describe('importAll', () => {
it('dispatches multiple fetchImport actions', async () => {
const OPTIONAL_STAGES = { stage1: true, stage2: false };
@@ -398,4 +423,51 @@ describe('import_projects store actions', () => {
);
});
});
+
+ describe('cancelImport', () => {
+ let mock;
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => mock.restore());
+
+ it('commits CANCEL_IMPORT_SUCCESS on success', async () => {
+ mock.onPost(MOCK_ENDPOINT).reply(200);
+
+ await testAction(
+ cancelImport,
+ { repoId: importRepoId },
+ localState,
+ [
+ {
+ type: CANCEL_IMPORT_SUCCESS,
+ payload: { repoId: 1 },
+ },
+ ],
+ [],
+ );
+ });
+
+ it('shows generic error message on an unsuccessful request', async () => {
+ mock.onPost(MOCK_ENDPOINT).reply(500);
+
+ await testAction(cancelImport, { repoId: importRepoId }, localState, [], []);
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Cancelling project import failed',
+ });
+ });
+
+ it('shows detailed error message on an unsuccessful request with errors fields in response', async () => {
+ const ERROR_MESSAGE = 'dummy';
+ mock.onPost(MOCK_ENDPOINT).reply(500, { errors: ERROR_MESSAGE });
+
+ await testAction(cancelImport, { repoId: importRepoId }, localState, [], []);
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Cancelling project import failed: ${ERROR_MESSAGE}`,
+ });
+ });
+ });
});
diff --git a/spec/frontend/import_entities/import_projects/store/getters_spec.js b/spec/frontend/import_entities/import_projects/store/getters_spec.js
index 110b692b222..fced5670f25 100644
--- a/spec/frontend/import_entities/import_projects/store/getters_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/getters_spec.js
@@ -1,6 +1,5 @@
import { STATUSES } from '~/import_entities/constants';
import {
- isLoading,
isImportingAnyRepo,
hasIncompatibleRepos,
hasImportableRepos,
@@ -31,24 +30,6 @@ describe('import_projects store getters', () => {
});
it.each`
- isLoadingRepos | isLoadingNamespaces | isLoadingValue
- ${false} | ${false} | ${false}
- ${true} | ${false} | ${true}
- ${false} | ${true} | ${true}
- ${true} | ${true} | ${true}
- `(
- 'isLoading returns $isLoadingValue when isLoadingRepos is $isLoadingRepos and isLoadingNamespaces is $isLoadingNamespaces',
- ({ isLoadingRepos, isLoadingNamespaces, isLoadingValue }) => {
- Object.assign(localState, {
- isLoadingRepos,
- isLoadingNamespaces,
- });
-
- expect(isLoading(localState)).toBe(isLoadingValue);
- },
- );
-
- it.each`
importStatus | value
${STATUSES.NONE} | ${false}
${STATUSES.SCHEDULING} | ${true}
diff --git a/spec/frontend/import_entities/import_projects/store/mutations_spec.js b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
index 77fae951300..7884e9b4307 100644
--- a/spec/frontend/import_entities/import_projects/store/mutations_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
@@ -27,7 +27,12 @@ describe('import_projects store mutations', () => {
state = {
filter: 'some-value',
repositories: ['some', ' repositories'],
- pageInfo: { page: 1 },
+ pageInfo: {
+ page: 1,
+ startCursor: 'Y3Vyc30yOjI2',
+ endCursor: 'Y3Vyc29yOjI1',
+ hasNextPage: false,
+ },
};
mutations[types.SET_FILTER](state, NEW_VALUE);
});
@@ -36,8 +41,11 @@ describe('import_projects store mutations', () => {
expect(state.repositories.length).toBe(0);
});
- it('resets current page to 0', () => {
+ it('resets pagintation', () => {
expect(state.pageInfo.page).toBe(0);
+ expect(state.pageInfo.startCursor).toBe(null);
+ expect(state.pageInfo.endCursor).toBe(null);
+ expect(state.pageInfo.hasNextPage).toBe(true);
});
});
@@ -263,43 +271,6 @@ describe('import_projects store mutations', () => {
});
});
- describe(`${types.REQUEST_NAMESPACES}`, () => {
- it('sets namespaces loading flag to true', () => {
- state = {};
-
- mutations[types.REQUEST_NAMESPACES](state);
-
- expect(state.isLoadingNamespaces).toBe(true);
- });
- });
-
- describe(`${types.RECEIVE_NAMESPACES_SUCCESS}`, () => {
- const response = [{ fullPath: 'some/path' }];
-
- beforeEach(() => {
- state = {};
- mutations[types.RECEIVE_NAMESPACES_SUCCESS](state, response);
- });
-
- it('stores namespaces to state', () => {
- expect(state.namespaces).toStrictEqual(response);
- });
-
- it('sets namespaces loading flag to false', () => {
- expect(state.isLoadingNamespaces).toBe(false);
- });
- });
-
- describe(`${types.RECEIVE_NAMESPACES_ERROR}`, () => {
- it('sets namespaces loading flag to false', () => {
- state = {};
-
- mutations[types.RECEIVE_NAMESPACES_ERROR](state);
-
- expect(state.isLoadingNamespaces).toBe(false);
- });
- });
-
describe(`${types.SET_IMPORT_TARGET}`, () => {
const PROJECT = {
id: 2,
@@ -345,4 +316,34 @@ describe('import_projects store mutations', () => {
expect(state.pageInfo.page).toBe(NEW_PAGE);
});
});
+
+ describe(`${types.SET_PAGE_CURSORS}`, () => {
+ it('sets page cursors', () => {
+ const NEW_CURSORS = { startCursor: 'startCur', endCursor: 'endCur', hasNextPage: false };
+ state = { pageInfo: { page: 1, startCursor: null, endCursor: null, hasNextPage: true } };
+
+ mutations[types.SET_PAGE_CURSORS](state, NEW_CURSORS);
+ expect(state.pageInfo).toEqual({ ...NEW_CURSORS, page: 1 });
+ });
+ });
+
+ describe(`${types.CANCEL_IMPORT_SUCCESS}`, () => {
+ const payload = { repoId: 1 };
+
+ beforeEach(() => {
+ state = {
+ repositories: [
+ {
+ importSource: { id: 1 },
+ importedProject: { importStatus: STATUSES.NONE },
+ },
+ ],
+ };
+ mutations[types.CANCEL_IMPORT_SUCCESS](state, payload);
+ });
+
+ it('updates project status', () => {
+ expect(state.repositories[0].importedProject.importStatus).toBe(STATUSES.CANCELED);
+ });
+ });
});
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
index 1b0253480e0..08c407cc4b4 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
@@ -1,5 +1,5 @@
import AxiosMockAdapter from 'axios-mock-adapter';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { ERROR_MSG } from '~/incidents_settings/constants';
import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service';
import axios from '~/lib/utils/axios_utils';
@@ -37,7 +37,7 @@ describe('IncidentsSettingsService', () => {
mock.onPatch().reply(httpStatusCodes.BAD_REQUEST);
return service.updateSettings({}).then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: expect.stringContaining(ERROR_MSG),
});
});
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index 5af0e272285..7589b04b0fd 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -7,11 +7,16 @@ import { mockField } from '../mock_data';
describe('DynamicField', () => {
let wrapper;
- const createComponent = (props, isInheriting = false) => {
+ const createComponent = (props, isInheriting = false, editable = true) => {
wrapper = mount(DynamicField, {
propsData: { ...mockField, ...props },
computed: {
isInheriting: () => isInheriting,
+ propsSource: () => {
+ return {
+ editable,
+ };
+ },
},
});
};
@@ -28,12 +33,14 @@ describe('DynamicField', () => {
describe('template', () => {
describe.each`
- isInheriting | disabled | readonly | checkboxLabel
- ${true} | ${'disabled'} | ${'readonly'} | ${undefined}
- ${false} | ${undefined} | ${undefined} | ${'Custom checkbox label'}
+ 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'}
`(
- 'dynamic field, when isInheriting = `%p`',
- ({ isInheriting, disabled, readonly, checkboxLabel }) => {
+ 'dynamic field, when isInheriting = `$isInheriting` and editable = `$editable`',
+ ({ isInheriting, editable, disabled, readonly, checkboxLabel }) => {
describe('type is checkbox', () => {
beforeEach(() => {
createComponent(
@@ -42,6 +49,7 @@ describe('DynamicField', () => {
checkboxLabel,
},
isInheriting,
+ editable,
);
});
@@ -74,6 +82,7 @@ describe('DynamicField', () => {
],
},
isInheriting,
+ editable,
);
});
@@ -97,6 +106,7 @@ describe('DynamicField', () => {
type: 'textarea',
},
isInheriting,
+ editable,
);
});
@@ -119,6 +129,7 @@ describe('DynamicField', () => {
type: 'password',
},
isInheriting,
+ editable,
);
});
@@ -143,6 +154,7 @@ describe('DynamicField', () => {
required: true,
},
isInheriting,
+ editable,
);
});
@@ -204,7 +216,7 @@ describe('DynamicField', () => {
});
expect(findGlFormGroup().find('small').html()).toContain(
- '[<code>1</code> <a>3</a> <a href="foo">4</a>]',
+ '[<code>1</code> <a>3</a> <a href="foo" target="_blank" rel="noopener noreferrer">4</a>',
);
});
});
diff --git a/spec/frontend/integrations/edit/components/integration_form_actions_spec.js b/spec/frontend/integrations/edit/components/integration_form_actions_spec.js
new file mode 100644
index 00000000000..e95e30a1899
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/integration_form_actions_spec.js
@@ -0,0 +1,227 @@
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
+import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
+import IntegrationFormActions from '~/integrations/edit/components/integration_form_actions.vue';
+
+import { integrationLevels } from '~/integrations/constants';
+import { createStore } from '~/integrations/edit/store';
+import { mockIntegrationProps } from '../mock_data';
+
+describe('IntegrationFormActions', () => {
+ let wrapper;
+
+ const createComponent = ({ customStateProps = {} } = {}) => {
+ const store = createStore({
+ customState: { ...mockIntegrationProps, ...customStateProps },
+ });
+ jest.spyOn(store, 'dispatch');
+
+ wrapper = shallowMountExtended(IntegrationFormActions, {
+ store,
+ propsData: {
+ hasSections: false,
+ },
+ });
+ };
+
+ const findConfirmationModal = () => wrapper.findComponent(ConfirmationModal);
+ const findResetConfirmationModal = () => wrapper.findComponent(ResetConfirmationModal);
+ const findResetButton = () => wrapper.findByTestId('reset-button');
+ const findSaveButton = () => wrapper.findByTestId('save-button');
+ const findTestButton = () => wrapper.findByTestId('test-button');
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+
+ describe('ConfirmationModal', () => {
+ it.each`
+ desc | integrationLevel | shouldRender
+ ${'Should'} | ${integrationLevels.INSTANCE} | ${true}
+ ${'Should'} | ${integrationLevels.GROUP} | ${true}
+ ${'Should not'} | ${integrationLevels.PROJECT} | ${false}
+ `(
+ '$desc render the ConfirmationModal when integrationLevel is "$integrationLevel"',
+ ({ integrationLevel, shouldRender }) => {
+ createComponent({
+ customStateProps: {
+ integrationLevel,
+ },
+ });
+ expect(findConfirmationModal().exists()).toBe(shouldRender);
+ },
+ );
+ });
+
+ describe('ResetConfirmationModal', () => {
+ it.each`
+ desc | integrationLevel | resetPath | shouldRender
+ ${'Should not'} | ${integrationLevels.INSTANCE} | ${''} | ${false}
+ ${'Should not'} | ${integrationLevels.GROUP} | ${''} | ${false}
+ ${'Should not'} | ${integrationLevels.PROJECT} | ${''} | ${false}
+ ${'Should'} | ${integrationLevels.INSTANCE} | ${'resetPath'} | ${true}
+ ${'Should'} | ${integrationLevels.GROUP} | ${'resetPath'} | ${true}
+ ${'Should not'} | ${integrationLevels.PROJECT} | ${'resetPath'} | ${false}
+ `(
+ '$desc render the ResetConfirmationModal modal when integrationLevel="$integrationLevel" and resetPath="$resetPath"',
+ ({ integrationLevel, resetPath, shouldRender }) => {
+ createComponent({
+ customStateProps: {
+ integrationLevel,
+ resetPath,
+ },
+ });
+ expect(findResetConfirmationModal().exists()).toBe(shouldRender);
+ },
+ );
+ });
+
+ describe('Buttons rendering', () => {
+ it.each`
+ integrationLevel | canTest | resetPath | saveBtn | testBtn | cancelBtn | resetBtn
+ ${integrationLevels.PROJECT} | ${true} | ${'resetPath'} | ${true} | ${true} | ${true} | ${false}
+ ${integrationLevels.PROJECT} | ${false} | ${'resetPath'} | ${true} | ${false} | ${true} | ${false}
+ ${integrationLevels.PROJECT} | ${true} | ${''} | ${true} | ${true} | ${true} | ${false}
+ ${integrationLevels.GROUP} | ${true} | ${'resetPath'} | ${true} | ${true} | ${true} | ${true}
+ ${integrationLevels.GROUP} | ${false} | ${'resetPath'} | ${true} | ${false} | ${true} | ${true}
+ ${integrationLevels.GROUP} | ${true} | ${''} | ${true} | ${true} | ${true} | ${false}
+ ${integrationLevels.INSTANCE} | ${true} | ${'resetPath'} | ${true} | ${true} | ${true} | ${true}
+ ${integrationLevels.INSTANCE} | ${false} | ${'resetPath'} | ${true} | ${false} | ${true} | ${true}
+ ${integrationLevels.INSTANCE} | ${true} | ${''} | ${true} | ${true} | ${true} | ${false}
+ `(
+ 'on $integrationLevel when canTest="$canTest" and resetPath="$resetPath"',
+ ({ integrationLevel, canTest, resetPath, saveBtn, testBtn, cancelBtn, resetBtn }) => {
+ createComponent({
+ customStateProps: {
+ integrationLevel,
+ canTest,
+ resetPath,
+ },
+ });
+
+ expect(findSaveButton().exists()).toBe(saveBtn);
+ expect(findTestButton().exists()).toBe(testBtn);
+ expect(findCancelButton().exists()).toBe(cancelBtn);
+ expect(findResetButton().exists()).toBe(resetBtn);
+ },
+ );
+ });
+
+ describe('interactions', () => {
+ describe('Save button clicked', () => {
+ const createAndSave = (integrationLevel, withModal = false) => {
+ createComponent({
+ customStateProps: {
+ integrationLevel,
+ canTest: true,
+ resetPath: 'resetPath',
+ },
+ });
+
+ findSaveButton().vm.$emit('click', new Event('click'));
+ if (withModal) {
+ findConfirmationModal().vm.$emit('submit');
+ }
+ wrapper.setProps({
+ isSaving: true,
+ });
+ };
+ const sharedFormStateTest = async (integrationLevel, withModal = false) => {
+ createAndSave(integrationLevel, withModal);
+
+ await nextTick();
+
+ const saveBtnWrapper = findSaveButton();
+ const testBtnWrapper = findTestButton();
+ const cancelBtnWrapper = findCancelButton();
+
+ expect(saveBtnWrapper.props('loading')).toBe(true);
+ expect(saveBtnWrapper.props('disabled')).toBe(true);
+
+ expect(testBtnWrapper.props('loading')).toBe(false);
+ expect(testBtnWrapper.props('disabled')).toBe(true);
+
+ expect(cancelBtnWrapper.props('loading')).toBe(false);
+ expect(cancelBtnWrapper.props('disabled')).toBe(true);
+ };
+
+ describe('on "project" level', () => {
+ const integrationLevel = integrationLevels.PROJECT;
+ it('emits the "save" event right away', async () => {
+ createAndSave(integrationLevel);
+ await nextTick();
+
+ expect(wrapper.emitted('save')).toHaveLength(1);
+ });
+
+ it('toggles the state of other buttons', async () => {
+ await sharedFormStateTest(integrationLevel);
+
+ const resetBtnWrapper = findResetButton();
+ expect(resetBtnWrapper.exists()).toBe(false);
+ });
+ });
+
+ describe.each([integrationLevels.INSTANCE, integrationLevels.GROUP])(
+ 'on "%s" level',
+ (integrationLevel) => {
+ it('emits the "save" event only after the confirmation', () => {
+ createComponent({
+ customStateProps: {
+ integrationLevel,
+ },
+ });
+
+ findSaveButton().vm.$emit('click', new Event('click'));
+ expect(wrapper.emitted('save')).toBeUndefined();
+
+ findConfirmationModal().vm.$emit('submit');
+ expect(wrapper.emitted('save')).toHaveLength(1);
+ });
+
+ it('toggles the state of other buttons', async () => {
+ await sharedFormStateTest(integrationLevel, true);
+
+ const resetBtnWrapper = findResetButton();
+ expect(resetBtnWrapper.props('loading')).toBe(false);
+ expect(resetBtnWrapper.props('disabled')).toBe(true);
+ });
+ },
+ );
+ });
+
+ describe('Reset button clicked', () => {
+ describe.each([integrationLevels.INSTANCE, integrationLevels.GROUP])(
+ 'on "%s" level',
+ (integrationLevel) => {
+ it('emits the "reset" event only after the confirmation', () => {
+ createComponent({
+ customStateProps: {
+ integrationLevel,
+ resetPath: 'resetPath',
+ },
+ });
+
+ findResetButton().vm.$emit('click', new Event('click'));
+ expect(wrapper.emitted('reset')).toBeUndefined();
+
+ findResetConfirmationModal().vm.$emit('reset');
+ expect(wrapper.emitted('reset')).toHaveLength(1);
+ });
+ },
+ );
+ });
+
+ describe('Test button clicked', () => {
+ it('emits the "test" event when clicked', () => {
+ createComponent({
+ customStateProps: {
+ integrationLevel: integrationLevels.PROJECT,
+ canTest: true,
+ },
+ });
+
+ findTestButton().vm.$emit('click', new Event('click'));
+ expect(wrapper.emitted('test')).toHaveLength(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 7e67379f5ab..4b49e492880 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,21 +1,20 @@
import { GlAlert, GlBadge, 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 { setHTMLFixture } from 'helpers/fixtures';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
-import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
-import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
import IntegrationSectionConnection from '~/integrations/edit/components/sections/connection.vue';
+import IntegrationFormActions from '~/integrations/edit/components/integration_form_actions.vue';
import {
- integrationLevels,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
INTEGRATION_FORM_TYPE_SLACK,
@@ -60,7 +59,6 @@ describe('IntegrationForm', () => {
stubs: {
OverrideDropdown,
ActiveCheckbox,
- ConfirmationModal,
TriggerFields,
},
mocks: {
@@ -73,12 +71,6 @@ describe('IntegrationForm', () => {
const findOverrideDropdown = () => wrapper.findComponent(OverrideDropdown);
const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox);
- const findConfirmationModal = () => wrapper.findComponent(ConfirmationModal);
- const findResetConfirmationModal = () => wrapper.findComponent(ResetConfirmationModal);
- const findResetButton = () => wrapper.findByTestId('reset-button');
- const findProjectSaveButton = () => wrapper.findByTestId('save-button');
- const findInstanceOrGroupSaveButton = () => wrapper.findByTestId('save-button-instance-group');
- const findTestButton = () => wrapper.findByTestId('test-button');
const findTriggerFields = () => wrapper.findComponent(TriggerFields);
const findAlert = () => wrapper.findComponent(GlAlert);
const findGlBadge = () => wrapper.findComponent(GlBadge);
@@ -91,6 +83,7 @@ describe('IntegrationForm', () => {
const findConnectionSectionComponent = () =>
findConnectionSection().findComponent(IntegrationSectionConnection);
const findHelpHtml = () => wrapper.findByTestId('help-html');
+ const findFormActions = () => wrapper.findComponent(IntegrationFormActions);
beforeEach(() => {
mockAxios = new MockAdapter(axios);
@@ -102,108 +95,6 @@ describe('IntegrationForm', () => {
});
describe('template', () => {
- describe('integrationLevel is instance', () => {
- it('renders ConfirmationModal', () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.INSTANCE,
- },
- });
-
- expect(findConfirmationModal().exists()).toBe(true);
- });
-
- describe('resetPath is empty', () => {
- it('does not render ResetConfirmationModal and button', () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.INSTANCE,
- },
- });
-
- expect(findResetButton().exists()).toBe(false);
- expect(findResetConfirmationModal().exists()).toBe(false);
- });
- });
-
- describe('resetPath is present', () => {
- it('renders ResetConfirmationModal and button', () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.INSTANCE,
- resetPath: 'resetPath',
- },
- });
-
- expect(findResetButton().exists()).toBe(true);
- expect(findResetConfirmationModal().exists()).toBe(true);
- });
- });
- });
-
- describe('integrationLevel is group', () => {
- it('renders ConfirmationModal', () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.GROUP,
- },
- });
-
- expect(findConfirmationModal().exists()).toBe(true);
- });
-
- describe('resetPath is empty', () => {
- it('does not render ResetConfirmationModal and button', () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.GROUP,
- },
- });
-
- expect(findResetButton().exists()).toBe(false);
- expect(findResetConfirmationModal().exists()).toBe(false);
- });
- });
-
- describe('resetPath is present', () => {
- it('renders ResetConfirmationModal and button', () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.GROUP,
- resetPath: 'resetPath',
- },
- });
-
- expect(findResetButton().exists()).toBe(true);
- expect(findResetConfirmationModal().exists()).toBe(true);
- });
- });
- });
-
- describe('integrationLevel is project', () => {
- it('does not render ConfirmationModal', () => {
- createComponent({
- customStateProps: {
- integrationLevel: 'project',
- },
- });
-
- expect(findConfirmationModal().exists()).toBe(false);
- });
-
- it('does not render ResetConfirmationModal and button', () => {
- createComponent({
- customStateProps: {
- integrationLevel: 'project',
- resetPath: 'resetPath',
- },
- });
-
- expect(findResetButton().exists()).toBe(false);
- expect(findResetConfirmationModal().exists()).toBe(false);
- });
- });
-
describe('triggerEvents is present', () => {
it('renders TriggerFields', () => {
const events = [{ title: 'push' }];
@@ -462,111 +353,85 @@ describe('IntegrationForm', () => {
);
});
- describe('when `save` button is clicked', () => {
- describe('buttons', () => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: true,
- },
- mountFn: mountExtended,
- });
-
- await findProjectSaveButton().vm.$emit('click', new Event('click'));
- });
-
- it('sets save button `loading` prop to `true`', () => {
- expect(findProjectSaveButton().props('loading')).toBe(true);
+ describe('Response to the "save" event (form submission)', () => {
+ const prepareComponentAndSave = async (initialActivated = true, checkValidityReturn) => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ initialActivated,
+ fields: [mockField],
+ },
+ mountFn: mountExtended,
});
+ jest.spyOn(findGlForm().element, 'submit');
+ jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(checkValidityReturn);
- it('sets test button `disabled` prop to `true`', () => {
- expect(findTestButton().props('disabled')).toBe(true);
- });
- });
+ findFormActions().vm.$emit('save');
+ await nextTick();
+ };
- describe.each`
- checkValidityReturn | integrationActive
- ${true} | ${false}
- ${true} | ${true}
- ${false} | ${false}
+ it.each`
+ desc | checkValidityReturn | integrationActive | shouldSubmit
+ ${'form is valid'} | ${true} | ${false} | ${true}
+ ${'form is valid'} | ${true} | ${true} | ${true}
+ ${'form is invalid'} | ${false} | ${false} | ${true}
+ ${'form is invalid'} | ${false} | ${true} | ${false}
`(
- 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
- ({ integrationActive, checkValidityReturn }) => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: integrationActive,
- },
- mountFn: mountExtended,
- });
- jest.spyOn(findGlForm().element, 'submit');
- jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(checkValidityReturn);
-
- await findProjectSaveButton().vm.$emit('click', new Event('click'));
- });
+ 'when $desc (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
+ async ({ integrationActive, checkValidityReturn, shouldSubmit }) => {
+ await prepareComponentAndSave(integrationActive, checkValidityReturn);
- it('submit form', () => {
+ if (shouldSubmit) {
expect(findGlForm().element.submit).toHaveBeenCalledTimes(1);
- });
+ } else {
+ expect(findGlForm().element.submit).not.toHaveBeenCalled();
+ }
},
);
- describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: true,
- fields: [mockField],
- },
- mountFn: mountExtended,
- });
- jest.spyOn(findGlForm().element, 'submit');
- jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false);
-
- await findProjectSaveButton().vm.$emit('click', new Event('click'));
- });
-
- it('does not submit form', () => {
- expect(findGlForm().element.submit).not.toHaveBeenCalled();
- });
+ it('flips `isSaving` to `true`', async () => {
+ await prepareComponentAndSave(true, true);
+ expect(findFormActions().props('isSaving')).toBe(true);
+ });
- it('sets save button `loading` prop to `false`', () => {
- expect(findProjectSaveButton().props('loading')).toBe(false);
+ describe('when form is invalid', () => {
+ beforeEach(async () => {
+ await prepareComponentAndSave(true, false);
});
- it('sets test button `disabled` prop to `false`', () => {
- expect(findTestButton().props('disabled')).toBe(false);
+ it('when form is invalid, it sets `isValidated` props on form fields', () => {
+ expect(findDynamicField().props('isValidated')).toBe(true);
});
- it('sets `isValidated` props on form fields', () => {
- expect(findDynamicField().props('isValidated')).toBe(true);
+ it('resets `isSaving`', () => {
+ expect(findFormActions().props('isSaving')).toBe(false);
});
});
});
- describe('when `test` button is clicked', () => {
+ describe('Response to the "test" event from the actions', () => {
describe('when form is invalid', () => {
- it('sets `isValidated` props on form fields', async () => {
+ beforeEach(async () => {
createComponent({
customStateProps: {
showActive: true,
- canTest: true,
fields: [mockField],
},
mountFn: mountExtended,
});
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false);
- await findTestButton().vm.$emit('click', new Event('click'));
+ findFormActions().vm.$emit('test');
+ await nextTick();
+ });
+ it('sets `isValidated` props on form fields', () => {
expect(findDynamicField().props('isValidated')).toBe(true);
});
+
+ it('resets `isTesting`', () => {
+ expect(findFormActions().props('isTesting')).toBe(false);
+ });
});
describe('when form is valid', () => {
@@ -576,26 +441,18 @@ describe('IntegrationForm', () => {
createComponent({
customStateProps: {
showActive: true,
- canTest: true,
testPath: mockTestPath,
},
mountFn: mountExtended,
});
+
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(true);
});
- describe('buttons', () => {
- beforeEach(async () => {
- await findTestButton().vm.$emit('click', new Event('click'));
- });
-
- it('sets test button `loading` prop to `true`', () => {
- expect(findTestButton().props('loading')).toBe(true);
- });
-
- it('sets save button `disabled` prop to `true`', () => {
- expect(findProjectSaveButton().props('disabled')).toBe(true);
- });
+ it('flips `isTesting` to `true`', async () => {
+ findFormActions().vm.$emit('test');
+ await nextTick();
+ expect(findFormActions().props('isTesting')).toBe(true);
});
describe.each`
@@ -614,7 +471,7 @@ describe('IntegrationForm', () => {
service_response: serviceResponse,
});
- await findTestButton().vm.$emit('click', new Event('click'));
+ findFormActions().vm.$emit('test');
await waitForPromises();
});
@@ -622,14 +479,6 @@ describe('IntegrationForm', () => {
expect(mockToastShow).toHaveBeenCalledWith(expectToast);
});
- it('sets `loading` prop of test button to `false`', () => {
- expect(findTestButton().props('loading')).toBe(false);
- });
-
- it('sets save button `disabled` prop to `false`', () => {
- expect(findProjectSaveButton().props('disabled')).toBe(false);
- });
-
it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => {
expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0);
});
@@ -638,44 +487,27 @@ describe('IntegrationForm', () => {
});
});
- describe('when `reset-confirmation-modal` emits `reset` event', () => {
+ describe('Response to the "reset" event from the actions', () => {
const mockResetPath = '/reset';
- describe('buttons', () => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.GROUP,
- canTest: true,
- resetPath: mockResetPath,
- },
- });
-
- await findResetConfirmationModal().vm.$emit('reset');
+ beforeEach(async () => {
+ mockAxios.onPost(mockResetPath).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ createComponent({
+ customStateProps: {
+ resetPath: mockResetPath,
+ },
});
- it('sets reset button `loading` prop to `true`', () => {
- expect(findResetButton().props('loading')).toBe(true);
- });
+ findFormActions().vm.$emit('reset');
+ await nextTick();
+ });
- it('sets other button `disabled` props to `true`', () => {
- expect(findInstanceOrGroupSaveButton().props('disabled')).toBe(true);
- expect(findTestButton().props('disabled')).toBe(true);
- });
+ it('flips `isResetting` to `true`', () => {
+ expect(findFormActions().props('isResetting')).toBe(true);
});
describe('when "reset settings" request fails', () => {
beforeEach(async () => {
- mockAxios.onPost(mockResetPath).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.GROUP,
- canTest: true,
- resetPath: mockResetPath,
- },
- });
-
- await findResetConfirmationModal().vm.$emit('reset');
await waitForPromises();
});
@@ -687,13 +519,8 @@ describe('IntegrationForm', () => {
expect(Sentry.captureException).toHaveBeenCalledTimes(1);
});
- it('sets reset button `loading` prop to `false`', () => {
- expect(findResetButton().props('loading')).toBe(false);
- });
-
- it('sets button `disabled` props to `false`', () => {
- expect(findInstanceOrGroupSaveButton().props('disabled')).toBe(false);
- expect(findTestButton().props('disabled')).toBe(false);
+ it('resets `isResetting`', () => {
+ expect(findFormActions().props('isResetting')).toBe(false);
});
});
@@ -702,64 +529,74 @@ describe('IntegrationForm', () => {
mockAxios.onPost(mockResetPath).replyOnce(httpStatus.OK);
createComponent({
customStateProps: {
- integrationLevel: integrationLevels.GROUP,
resetPath: mockResetPath,
},
});
- await findResetConfirmationModal().vm.$emit('reset');
+ findFormActions().vm.$emit('reset');
await waitForPromises();
});
it('calls `refreshCurrentPage`', () => {
expect(refreshCurrentPage).toHaveBeenCalledTimes(1);
});
- });
- describe('Slack integration', () => {
- describe('Help and sections rendering', () => {
- const dummyHelp = 'Foo Help';
-
- it.each`
- integration | flagIsOn | helpHtml | sections | shouldShowSections | shouldShowHelp
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${''} | ${[]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${''} | ${[]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true}
- ${'foo'} | ${false} | ${''} | ${[]} | ${false} | ${false}
- ${'foo'} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${'foo'} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${true} | ${''} | ${[]} | ${false} | ${false}
- ${'foo'} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${'foo'} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
- `(
- '$sections sections, and "$helpHtml" helpHtml when the FF is "$flagIsOn" for "$integration" integration',
- ({ integration, flagIsOn, helpHtml, sections, shouldShowSections, shouldShowHelp }) => {
- createComponent({
- provide: {
- helpHtml,
- glFeatures: { integrationSlackAppNotifications: flagIsOn },
- },
- customStateProps: {
- sections,
- type: integration,
- },
- });
- expect(findAllSections().length > 0).toEqual(shouldShowSections);
- expect(findHelpHtml().exists()).toBe(shouldShowHelp);
- if (shouldShowHelp) {
- expect(findHelpHtml().html()).toContain(helpHtml);
- }
- },
- );
+ it('resets `isResetting`', async () => {
+ expect(findFormActions().props('isResetting')).toBe(false);
});
+ });
+ });
+
+ describe('Slack integration', () => {
+ describe('Help and sections rendering', () => {
+ const dummyHelp = 'Foo Help';
+
+ it.each`
+ integration | flagIsOn | helpHtml | sections | shouldShowSections | shouldShowHelp
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${''} | ${[]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${''} | ${[]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true}
+ ${'foo'} | ${false} | ${''} | ${[]} | ${false} | ${false}
+ ${'foo'} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${'foo'} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${true} | ${''} | ${[]} | ${false} | ${false}
+ ${'foo'} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${'foo'} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
+ `(
+ '$sections sections, and "$helpHtml" helpHtml when the FF is "$flagIsOn" for "$integration" integration',
+ ({ integration, flagIsOn, helpHtml, sections, shouldShowSections, shouldShowHelp }) => {
+ createComponent({
+ provide: {
+ helpHtml,
+ glFeatures: { integrationSlackAppNotifications: flagIsOn },
+ },
+ customStateProps: {
+ sections,
+ type: integration,
+ },
+ });
+ expect(findAllSections().length > 0).toEqual(shouldShowSections);
+ expect(findHelpHtml().exists()).toBe(shouldShowHelp);
+ if (shouldShowHelp) {
+ expect(findHelpHtml().html()).toContain(helpHtml);
+ }
+ },
+ );
+ });
+ describe.each`
+ hasSections | hasFieldsWithoutSections | description
+ ${true} | ${true} | ${'When having both: the sections and the fields without a section'}
+ ${true} | ${false} | ${'When having the sections only'}
+ ${false} | ${true} | ${'When having only the fields without a section'}
+ `('$description', ({ hasSections, hasFieldsWithoutSections }) => {
it.each`
prefix | integration | shouldUpgradeSlack | flagIsOn | shouldShowAlert
${'does'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${true} | ${true}
@@ -769,7 +606,7 @@ describe('IntegrationForm', () => {
${'does not'} | ${'foo'} | ${false} | ${true} | ${false}
${'does not'} | ${'foo'} | ${true} | ${false} | ${false}
`(
- '$prefix render the upgrade warnning when we are in "$integration" integration with the flag "$flagIsOn" and Slack-needs-upgrade is "$shouldUpgradeSlack"',
+ '$prefix render the upgrade warning when we are in "$integration" integration with the flag "$flagIsOn" and Slack-needs-upgrade is "$shouldUpgradeSlack" and have sections',
({ integration, shouldUpgradeSlack, flagIsOn, shouldShowAlert }) => {
createComponent({
provide: {
@@ -778,7 +615,8 @@ describe('IntegrationForm', () => {
customStateProps: {
shouldUpgradeSlack,
type: integration,
- sections: [mockSectionConnection],
+ sections: hasSections ? [mockSectionConnection] : [],
+ fields: hasFieldsWithoutSections ? [mockField] : [],
},
});
expect(findAlert().exists()).toBe(shouldShowAlert);
diff --git a/spec/frontend/invite_members/components/import_project_members_modal_spec.js b/spec/frontend/invite_members/components/import_project_members_modal_spec.js
index 8b2d13be309..d839cde163c 100644
--- a/spec/frontend/invite_members/components/import_project_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/import_project_members_modal_spec.js
@@ -8,6 +8,12 @@ import * as ProjectsApi from '~/api/projects_api';
import ImportProjectMembersModal from '~/invite_members/components/import_project_members_modal.vue';
import ProjectSelect from '~/invite_members/components/project_select.vue';
import axios from '~/lib/utils/axios_utils';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '~/invite_members/utils/trigger_successful_invite_alert';
+
+jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
let wrapper;
let mock;
@@ -19,11 +25,12 @@ const $toast = {
show: jest.fn(),
};
-const createComponent = () => {
+const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(ImportProjectMembersModal, {
propsData: {
projectId,
projectName,
+ ...props,
},
stubs: {
GlModal: stubComponent(GlModal, {
@@ -101,6 +108,35 @@ describe('ImportProjectMembersModal', () => {
});
describe('submitting the import', () => {
+ describe('when the import is successful with reloadPageOnSubmit', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { reloadPageOnSubmit: true },
+ });
+
+ findProjectSelect().vm.$emit('input', projectToBeImported);
+
+ jest.spyOn(ProjectsApi, 'importProjectMembers').mockResolvedValue();
+
+ clickImportButton();
+ });
+
+ it('calls displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).toHaveBeenCalled();
+ });
+
+ it('calls reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).toHaveBeenCalled();
+ });
+
+ it('does not display the successful toastMessage', () => {
+ expect($toast.show).not.toHaveBeenCalledWith(
+ 'Successfully imported',
+ wrapper.vm.$options.toastOptions,
+ );
+ });
+ });
+
describe('when the import is successful', () => {
beforeEach(() => {
createComponent();
@@ -126,6 +162,14 @@ describe('ImportProjectMembersModal', () => {
);
});
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
+
it('sets isLoading to false after success', () => {
expect(findGlModal().props('actionPrimary').attributes.loading).toBe(false);
});
diff --git a/spec/frontend/invite_members/components/invite_group_notification_spec.js b/spec/frontend/invite_members/components/invite_group_notification_spec.js
new file mode 100644
index 00000000000..3e6ba6da9f4
--- /dev/null
+++ b/spec/frontend/invite_members/components/invite_group_notification_spec.js
@@ -0,0 +1,42 @@
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { sprintf } from '~/locale';
+import InviteGroupNotification from '~/invite_members/components/invite_group_notification.vue';
+import { GROUP_MODAL_ALERT_BODY } from '~/invite_members/constants';
+
+describe('InviteGroupNotification', () => {
+ let wrapper;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(InviteGroupNotification, {
+ provide: { freeUsersLimit: 5 },
+ propsData: { name: 'name' },
+ stubs: { GlSprintf },
+ });
+ };
+
+ describe('when rendering', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('passes the correct props', () => {
+ expect(findAlert().props()).toMatchObject({ variant: 'warning', dismissible: false });
+ });
+
+ it('shows the correct message', () => {
+ const message = sprintf(GROUP_MODAL_ALERT_BODY, { count: 5 });
+
+ expect(findAlert().text()).toMatchInterpolatedText(message);
+ });
+
+ it('has a help link', () => {
+ expect(findLink().attributes('href')).toEqual(
+ 'https://docs.gitlab.com/ee/user/group/manage.html#share-a-group-with-another-group',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
index f9cb4a149f2..c2a55517405 100644
--- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
@@ -6,9 +6,16 @@ import InviteGroupsModal from '~/invite_members/components/invite_groups_modal.v
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import GroupSelect from '~/invite_members/components/group_select.vue';
+import InviteGroupNotification from '~/invite_members/components/invite_group_notification.vue';
import { stubComponent } from 'helpers/stub_component';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '~/invite_members/utils/trigger_successful_invite_alert';
import { propsData, sharedGroup } from '../mock_data/group_modal';
+jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
+
describe('InviteGroupsModal', () => {
let wrapper;
@@ -44,6 +51,7 @@ describe('InviteGroupsModal', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findGroupSelect = () => wrapper.findComponent(GroupSelect);
+ const findInviteGroupAlert = () => wrapper.findComponent(InviteGroupNotification);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
@@ -74,6 +82,20 @@ describe('InviteGroupsModal', () => {
});
});
+ describe('rendering the invite group notification', () => {
+ it('shows the user limit notification alert when free user cap is enabled', () => {
+ createComponent({ freeUserCapEnabled: true });
+
+ expect(findInviteGroupAlert().exists()).toBe(true);
+ });
+
+ it('does not show the user limit notification alert', () => {
+ createComponent();
+
+ expect(findInviteGroupAlert().exists()).toBe(false);
+ });
+ });
+
describe('submitting the invite form', () => {
let apiResolve;
let apiReject;
@@ -126,6 +148,14 @@ describe('InviteGroupsModal', () => {
onComplete: expect.any(Function),
});
});
+
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
});
describe('when fails', () => {
@@ -156,4 +186,37 @@ describe('InviteGroupsModal', () => {
});
});
});
+
+ describe('submitting the invite form with reloadPageOnSubmit set true', () => {
+ const groupPostData = {
+ group_id: sharedGroup.id,
+ group_access: propsData.defaultAccessLevel,
+ expires_at: undefined,
+ format: 'json',
+ };
+
+ beforeEach(() => {
+ createComponent({ reloadPageOnSubmit: true });
+ triggerGroupSelect(sharedGroup);
+
+ wrapper.vm.$toast = { show: jest.fn() };
+ jest.spyOn(Api, 'groupShareWithGroup').mockResolvedValue({ data: groupPostData });
+
+ clickInviteButton();
+ });
+
+ describe('when succeeds', () => {
+ it('calls displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).toHaveBeenCalled();
+ });
+
+ it('calls reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).toHaveBeenCalled();
+ });
+
+ it('does not show the toast message on failure', () => {
+ expect(wrapper.vm.$toast.show).not.toHaveBeenCalled();
+ });
+ });
+ });
});
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 47be1933ed7..22fcedb2eaf 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -19,13 +19,17 @@ import {
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
LEARN_GITLAB,
EXPANDED_ERRORS,
- EMPTY_INVITES_ERROR_TEXT,
+ EMPTY_INVITES_ALERT_TEXT,
} from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, { HTTP_STATUS_CREATED } from '~/lib/utils/http_status';
import { getParameterValues } from '~/lib/utils/url_utility';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '~/invite_members/utils/trigger_successful_invite_alert';
import { GROUPS_INVITATIONS_PATH, invitationsApiResponse } from '../mock_data/api_responses';
import {
propsData,
@@ -40,6 +44,7 @@ import {
GlEmoji,
} from '../mock_data/member_modal';
+jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
jest.mock('~/experimentation/experiment_tracking');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -57,6 +62,7 @@ describe('InviteMembersModal', () => {
},
propsData: {
usersLimitDataset: {},
+ fullPath: 'project',
...propsData,
...props,
},
@@ -95,6 +101,7 @@ describe('InviteMembersModal', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findBase = () => wrapper.findComponent(InviteModalBase);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
+ const findEmptyInvitesAlert = () => wrapper.findByTestId('empty-invites-alert');
const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error');
const findMoreInviteErrorsButton = () => wrapper.findByTestId('accordion-button');
const findUserLimitAlert = () => wrapper.findComponent(UserLimitNotification);
@@ -397,7 +404,8 @@ describe('InviteMembersModal', () => {
await waitForPromises();
- expect(membersFormGroupInvalidFeedback()).toBe(EMPTY_INVITES_ERROR_TEXT);
+ expect(findEmptyInvitesAlert().text()).toBe(EMPTY_INVITES_ALERT_TEXT);
+ expect(membersFormGroupInvalidFeedback()).toBe(MEMBERS_PLACEHOLDER);
expect(findMembersSelect().props('exceptionState')).toBe(false);
await triggerMembersTokenSelect([user1]);
@@ -417,6 +425,29 @@ describe('InviteMembersModal', () => {
tasks_project_id: '',
};
+ describe('when reloadOnSubmit is true', () => {
+ beforeEach(async () => {
+ createComponent({ reloadPageOnSubmit: true });
+ await triggerMembersTokenSelect([user1, user2]);
+
+ wrapper.vm.$toast = { show: jest.fn() };
+ jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
+ clickInviteButton();
+ });
+
+ it('calls displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).toHaveBeenCalled();
+ });
+
+ it('calls reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).toHaveBeenCalled();
+ });
+
+ it('does not show the toast message', () => {
+ expect(wrapper.vm.$toast.show).not.toHaveBeenCalled();
+ });
+ });
+
describe('when member is added successfully', () => {
beforeEach(async () => {
createComponent();
@@ -438,6 +469,14 @@ describe('InviteMembersModal', () => {
it('displays the successful toastMessage', () => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added');
});
+
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
});
describe('when opened from a Learn GitLab page', () => {
@@ -464,7 +503,7 @@ describe('InviteMembersModal', () => {
describe('clearing the invalid state and message', () => {
beforeEach(async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN);
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_TAKEN);
clickInviteButton();
@@ -523,7 +562,7 @@ describe('InviteMembersModal', () => {
});
it('displays the restricted user api message for response with bad request', async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
clickInviteButton();
@@ -536,7 +575,7 @@ describe('InviteMembersModal', () => {
});
it('displays all errors when there are multiple existing users that are restricted by email', async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
clickInviteButton();
@@ -590,6 +629,14 @@ describe('InviteMembersModal', () => {
it('displays the successful toastMessage', () => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added');
});
+
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
});
});
@@ -633,7 +680,7 @@ describe('InviteMembersModal', () => {
});
it('displays the restricted email error when restricted email is invited', async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
clickInviteButton();
@@ -647,7 +694,7 @@ describe('InviteMembersModal', () => {
});
it('displays all errors when there are multiple emails that return a restricted error message', async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
clickInviteButton();
@@ -677,6 +724,14 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
expect(findMembersSelect().props('exceptionState')).toBe(false);
});
+
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
});
describe('when multiple emails are invited at the same time', () => {
@@ -698,7 +753,7 @@ describe('InviteMembersModal', () => {
createInviteMembersToGroupWrapper();
await triggerMembersTokenSelect([user3, user4, user5, user6]);
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EXPANDED_RESTRICTED);
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EXPANDED_RESTRICTED);
clickInviteButton();
@@ -791,6 +846,14 @@ describe('InviteMembersModal', () => {
it('displays the successful toastMessage', () => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added');
});
+
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
});
it('calls Apis with the invite source passed through to openModal', async () => {
diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js
index aeead8809fd..db2afbbd141 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -1,16 +1,15 @@
import {
- GlDropdown,
- GlDropdownItem,
+ GlFormSelect,
GlDatepicker,
GlFormGroup,
- GlSprintf,
GlLink,
+ GlSprintf,
GlModal,
GlIcon,
} from '@gitlab/ui';
import { stubComponent } from 'helpers/stub_component';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
@@ -26,24 +25,30 @@ import { propsData, membersPath, purchasePath } from '../mock_data/modal_base';
describe('InviteModalBase', () => {
let wrapper;
- const createComponent = (props = {}, stubs = {}) => {
- wrapper = shallowMountExtended(InviteModalBase, {
+ const createComponent = ({ props = {}, stubs = {}, mountFn = shallowMountExtended } = {}) => {
+ const requiredStubs =
+ mountFn === mountExtended
+ ? {}
+ : {
+ ContentTransition,
+ GlFormSelect: true,
+ GlSprintf,
+ GlFormGroup: stubComponent(GlFormGroup, {
+ props: ['state', 'invalidFeedback'],
+ }),
+ };
+
+ wrapper = mountFn(InviteModalBase, {
propsData: {
...propsData,
...props,
},
stubs: {
- ContentTransition,
GlModal: stubComponent(GlModal, {
template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
}),
- GlDropdown: true,
- GlDropdownItem: true,
- GlSprintf,
- GlFormGroup: stubComponent(GlFormGroup, {
- props: ['state', 'invalidFeedback'],
- }),
+ ...requiredStubs,
...stubs,
},
});
@@ -51,11 +56,10 @@ describe('InviteModalBase', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
+ const findFormSelect = () => wrapper.findComponent(GlFormSelect);
+ const findFormSelectOptions = () => findFormSelect().findAllComponents('option');
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
const findLink = () => wrapper.findComponent(GlLink);
const findIcon = () => wrapper.findComponent(GlIcon);
@@ -97,16 +101,29 @@ describe('InviteModalBase', () => {
});
describe('rendering the access levels dropdown', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ });
+ });
+
it('sets the default dropdown text to the default access level name', () => {
- expect(findDropdown().attributes('text')).toBe('Guest');
+ expect(findFormSelect().exists()).toBe(true);
+ expect(findFormSelect().element.value).toBe('10');
});
it('renders dropdown items for each accessLevel', () => {
- expect(findDropdownItems()).toHaveLength(5);
+ expect(findFormSelectOptions()).toHaveLength(5);
});
});
describe('rendering the help link', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ });
+ });
+
it('renders the correct link', () => {
expect(findLink().attributes('href')).toBe(propsData.helpLink);
});
@@ -126,7 +143,7 @@ describe('InviteModalBase', () => {
});
it('renders description', () => {
- createComponent({}, { GlFormGroup });
+ createComponent({ stubs: { GlFormGroup } });
expect(findMembersFormGroup().attributes('description')).toContain(
propsData.formGroupDescription,
@@ -144,13 +161,16 @@ describe('InviteModalBase', () => {
beforeEach(() => {
createComponent(
- { usersLimitDataset: { membersPath, purchasePath, reachedLimit: true } },
- { GlModal, GlFormGroup },
+ { props: { usersLimitDataset: { membersPath, purchasePath, reachedLimit: true } } },
+ { stubs: { GlModal, GlFormGroup } },
);
});
it('tracks actions', () => {
- createComponent({ usersLimitDataset: { reachedLimit: true } }, { GlFormGroup, GlModal });
+ createComponent({
+ props: { usersLimitDataset: { reachedLimit: true } },
+ stubs: { GlFormGroup, GlModal },
+ });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
const modal = wrapper.findComponent(GlModal);
@@ -164,8 +184,8 @@ describe('InviteModalBase', () => {
describe('when user limit is close on a personal namespace', () => {
beforeEach(() => {
- createComponent(
- {
+ createComponent({
+ props: {
usersLimitDataset: {
membersPath,
userNamespace: true,
@@ -173,8 +193,8 @@ describe('InviteModalBase', () => {
reachedLimit: false,
},
},
- { GlModal, GlFormGroup },
- );
+ stubs: { GlModal, GlFormGroup },
+ });
});
it('renders correct buttons', () => {
@@ -190,16 +210,16 @@ describe('InviteModalBase', () => {
});
describe('when users limit is not reached', () => {
- const textRegex = /Select a role.+Read more about role permissions Access expiration date \(optional\)/;
+ const textRegex = /Select a role\s*Read more about role permissions\s*Access expiration date \(optional\)/;
beforeEach(() => {
- createComponent({ reachedLimit: false }, { GlModal, GlFormGroup });
+ createComponent({ props: { reachedLimit: false }, stubs: { GlModal, GlFormGroup } });
});
it('renders correct blocks', () => {
expect(findIcon().exists()).toBe(false);
expect(findDisabledInput().exists()).toBe(false);
- expect(findDropdown().exists()).toBe(true);
+ expect(findFormSelect().exists()).toBe(true);
expect(findDatepicker().exists()).toBe(true);
expect(wrapper.findComponent(GlModal).text()).toMatch(textRegex);
});
@@ -213,7 +233,9 @@ describe('InviteModalBase', () => {
it('with isLoading, shows loading for invite button', () => {
createComponent({
- isLoading: true,
+ props: {
+ isLoading: true,
+ },
});
expect(wrapper.findComponent(GlModal).props('actionPrimary').attributes.loading).toBe(true);
@@ -221,7 +243,9 @@ describe('InviteModalBase', () => {
it('with invalidFeedbackMessage, set members form group exception state', () => {
createComponent({
- invalidFeedbackMessage: 'invalid message!',
+ props: {
+ invalidFeedbackMessage: 'invalid message!',
+ },
});
expect(findMembersFormGroup().props()).toEqual({
diff --git a/spec/frontend/invite_members/mock_data/group_modal.js b/spec/frontend/invite_members/mock_data/group_modal.js
index c8588683885..65e8b025dd9 100644
--- a/spec/frontend/invite_members/mock_data/group_modal.js
+++ b/spec/frontend/invite_members/mock_data/group_modal.js
@@ -7,6 +7,8 @@ export const propsData = {
accessLevels: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
defaultAccessLevel: 10,
helpLink: 'https://example.com',
+ fullPath: 'project',
+ freeUserCapEnabled: false,
};
export const sharedGroup = { id: '981' };
diff --git a/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
new file mode 100644
index 00000000000..38b16dd0c2c
--- /dev/null
+++ b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
@@ -0,0 +1,54 @@
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '~/invite_members/utils/trigger_successful_invite_alert';
+import {
+ TOAST_MESSAGE_LOCALSTORAGE_KEY,
+ TOAST_MESSAGE_SUCCESSFUL,
+} from '~/invite_members/constants';
+import { createAlert } from '~/flash';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+
+jest.mock('~/flash');
+useLocalStorageSpy();
+
+describe('Display Successful Invitation Alert', () => {
+ it('does not show alert if localStorage key not present', () => {
+ localStorage.removeItem(TOAST_MESSAGE_LOCALSTORAGE_KEY);
+
+ displaySuccessfulInvitationAlert();
+
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+
+ it('shows alert when localStorage key is present', () => {
+ localStorage.setItem(TOAST_MESSAGE_LOCALSTORAGE_KEY, 'true');
+
+ displaySuccessfulInvitationAlert();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: TOAST_MESSAGE_SUCCESSFUL,
+ variant: 'info',
+ });
+ });
+});
+
+describe('Reload On Invitation Success', () => {
+ const { location } = window;
+
+ beforeAll(() => {
+ delete window.location;
+ window.location = { reload: jest.fn() };
+ });
+
+ afterAll(() => {
+ window.location = location;
+ });
+
+ it('sets localStorage value and calls window.location.reload', () => {
+ reloadOnInvitationSuccess();
+
+ expect(localStorage.setItem).toHaveBeenCalledWith(TOAST_MESSAGE_LOCALSTORAGE_KEY, 'true');
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+});
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 3f72396cce6..3f40772f7fc 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -1,58 +1,380 @@
import { 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 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';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import {
+ filteredTokens,
+ locationSearch,
+ setSortPreferenceMutationResponse,
+ setSortPreferenceMutationResponseWithErrors,
+} from 'jest/issues/list/mock_data';
import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue';
+import { CREATED_DESC, i18n, UPDATED_DESC, urlSortParams } from '~/issues/list/constants';
+import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
+import { getSortKey, getSortOptions } from '~/issues/list/utils';
+import axios from '~/lib/utils/axios_utils';
+import { scrollUp } from '~/lib/utils/scroll_utils';
+import {
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableStates } from '~/vue_shared/issuable/list/constants';
+import { emptyIssuesQueryResponse, issuesQueryResponse } from '../mock_data';
+
+jest.mock('@sentry/browser');
+jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
describe('IssuesDashboardApp component', () => {
+ let axiosMock;
let wrapper;
+ Vue.use(VueApollo);
+
const defaultProvide = {
calendarPath: 'calendar/path',
emptyStateSvgPath: 'empty-state.svg',
+ hasBlockedIssuesFeature: true,
+ hasIssuableHealthStatusFeature: true,
+ hasIssueWeightsFeature: true,
+ hasScopedLabelsFeature: true,
+ initialSort: CREATED_DESC,
+ isPublicVisibilityRestricted: false,
isSignedIn: true,
rssPath: 'rss/path',
};
+ let defaultQueryResponse = issuesQueryResponse;
+ if (IS_EE) {
+ defaultQueryResponse = cloneDeep(issuesQueryResponse);
+ defaultQueryResponse.data.issues.nodes[0].blockingCount = 1;
+ defaultQueryResponse.data.issues.nodes[0].healthStatus = null;
+ defaultQueryResponse.data.issues.nodes[0].weight = 5;
+ }
+
const findCalendarButton = () =>
wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.calendarButtonText });
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findIssuableList = () => wrapper.findComponent(IssuableList);
+ const findIssueCardStatistics = () => wrapper.findComponent(IssueCardStatistics);
+ const findIssueCardTimeInfo = () => wrapper.findComponent(IssueCardTimeInfo);
const findRssButton = () =>
wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.rssButtonText });
- const mountComponent = () => {
- wrapper = mountExtended(IssuesDashboardApp, { provide: defaultProvide });
+ const mountComponent = ({
+ provide = {},
+ issuesQueryHandler = jest.fn().mockResolvedValue(defaultQueryResponse),
+ sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
+ } = {}) => {
+ wrapper = mountExtended(IssuesDashboardApp, {
+ apolloProvider: createMockApollo([
+ [getIssuesQuery, issuesQueryHandler],
+ [setSortPreferenceMutation, sortPreferenceMutationResponse],
+ ]),
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ });
};
beforeEach(() => {
- mountComponent();
+ setWindowLocation(TEST_HOST);
+ axiosMock = new AxiosMockAdapter(axios);
});
- it('renders IssuableList component', () => {
+ afterEach(() => {
+ axiosMock.reset();
+ });
+
+ it('renders IssuableList component', async () => {
+ mountComponent();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+
expect(findIssuableList().props()).toMatchObject({
currentTab: IssuableStates.Opened,
+ hasNextPage: true,
+ hasPreviousPage: false,
+ hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature,
+ initialSortBy: CREATED_DESC,
+ issuables: issuesQueryResponse.data.issues.nodes,
+ issuablesLoading: false,
namespace: 'dashboard',
recentSearchesStorageKey: 'issues',
searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder,
+ showPaginationControls: true,
+ sortOptions: getSortOptions({
+ hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
+ }),
tabs: IssuesDashboardApp.IssuableListTabs,
+ urlParams: {
+ sort: urlSortParams[CREATED_DESC],
+ state: IssuableStates.Opened,
+ },
+ useKeysetPagination: true,
});
});
it('renders RSS button link', () => {
+ mountComponent();
+
expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
expect(findRssButton().props('icon')).toBe('rss');
});
it('renders calendar button link', () => {
+ mountComponent();
+
expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
expect(findCalendarButton().props('icon')).toBe('calendar');
});
- it('renders empty state', () => {
+ it('renders issue time information', async () => {
+ mountComponent();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+
+ expect(findIssueCardTimeInfo().exists()).toBe(true);
+ });
+
+ it('renders issue statistics', async () => {
+ mountComponent();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+
+ expect(findIssueCardStatistics().exists()).toBe(true);
+ });
+
+ it('renders empty state', async () => {
+ mountComponent({ issuesQueryHandler: jest.fn().mockResolvedValue(emptyIssuesQueryResponse) });
+ await waitForPromises();
+
expect(findEmptyState().props()).toMatchObject({
svgPath: defaultProvide.emptyStateSvgPath,
title: IssuesDashboardApp.i18n.emptyStateTitle,
});
});
+
+ describe('initial url params', () => {
+ describe('search', () => {
+ it('is set from the url params', () => {
+ setWindowLocation(locationSearch);
+ mountComponent();
+
+ expect(findIssuableList().props('urlParams')).toMatchObject({ search: 'find issues' });
+ });
+ });
+
+ describe('sort', () => {
+ describe('when initial sort value uses old enum values', () => {
+ const oldEnumSortValues = Object.values(urlSortParams);
+
+ it.each(oldEnumSortValues)('initial sort is set with value %s', (sort) => {
+ mountComponent({ provide: { initialSort: sort } });
+
+ expect(findIssuableList().props('initialSortBy')).toBe(getSortKey(sort));
+ });
+ });
+
+ describe('when initial sort value uses new GraphQL enum values', () => {
+ const graphQLEnumSortValues = Object.keys(urlSortParams);
+
+ it.each(graphQLEnumSortValues)('initial sort is set with value %s', (sort) => {
+ mountComponent({ provide: { initialSort: sort.toLowerCase() } });
+
+ expect(findIssuableList().props('initialSortBy')).toBe(sort);
+ });
+ });
+
+ describe('when initial sort value is invalid', () => {
+ it.each(['', 'asdf', null, undefined])(
+ 'initial sort is set to value CREATED_DESC',
+ (sort) => {
+ mountComponent({ provide: { initialSort: sort } });
+
+ expect(findIssuableList().props('initialSortBy')).toBe(CREATED_DESC);
+ },
+ );
+ });
+ });
+
+ describe('state', () => {
+ it('is set from the url params', () => {
+ const initialState = IssuableStates.All;
+ setWindowLocation(`?state=${initialState}`);
+ mountComponent();
+
+ expect(findIssuableList().props('currentTab')).toBe(initialState);
+ });
+ });
+
+ describe('filter tokens', () => {
+ it('is set from the url params', () => {
+ setWindowLocation(locationSearch);
+ mountComponent();
+
+ expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens);
+ });
+ });
+ });
+
+ describe('when there is an error fetching issues', () => {
+ beforeEach(() => {
+ mountComponent({ issuesQueryHandler: jest.fn().mockRejectedValue(new Error('ERROR')) });
+ jest.runOnlyPendingTimers();
+ return waitForPromises();
+ });
+
+ it('shows an error message', () => {
+ expect(findIssuableList().props('error')).toBe(i18n.errorFetchingIssues);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
+ });
+
+ it('clears error message when "dismiss-alert" event is emitted from IssuableList', async () => {
+ findIssuableList().vm.$emit('dismiss-alert');
+ await nextTick();
+
+ expect(findIssuableList().props('error')).toBeNull();
+ });
+ });
+
+ describe('tokens', () => {
+ const mockCurrentUser = {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ avatar_url: 'avatar/url',
+ };
+ const originalGon = window.gon;
+
+ beforeEach(() => {
+ window.gon = {
+ ...originalGon,
+ current_user_id: mockCurrentUser.id,
+ current_user_fullname: mockCurrentUser.name,
+ current_username: mockCurrentUser.username,
+ current_user_avatar_url: mockCurrentUser.avatar_url,
+ };
+ mountComponent();
+ });
+
+ afterEach(() => {
+ window.gon = originalGon;
+ });
+
+ it('renders all tokens alphabetically', () => {
+ const preloadedUsers = [{ ...mockCurrentUser, id: mockCurrentUser.id }];
+
+ expect(findIssuableList().props('searchTokens')).toMatchObject([
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
+ { type: TOKEN_TYPE_AUTHOR, preloadedUsers },
+ ]);
+ });
+ });
+
+ describe('events', () => {
+ describe('when "click-tab" event is emitted by IssuableList', () => {
+ beforeEach(() => {
+ mountComponent();
+
+ findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
+ });
+
+ it('updates ui to the new tab', () => {
+ expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
+ });
+
+ it('updates url to the new tab', () => {
+ expect(findIssuableList().props('urlParams')).toMatchObject({
+ state: IssuableStates.Closed,
+ });
+ });
+ });
+
+ describe.each(['next-page', 'previous-page'])(
+ 'when "%s" event is emitted by IssuableList',
+ (event) => {
+ beforeEach(() => {
+ mountComponent();
+
+ findIssuableList().vm.$emit(event);
+ });
+
+ it('scrolls to the top', () => {
+ expect(scrollUp).toHaveBeenCalled();
+ });
+ },
+ );
+
+ describe('when "sort" event is emitted by IssuableList', () => {
+ it.each(Object.keys(urlSortParams))(
+ 'updates to the new sort when payload is `%s`',
+ async (sortKey) => {
+ // Ensure initial sort key is different so we can trigger an update when emitting a sort key
+ if (sortKey === CREATED_DESC) {
+ mountComponent({ provide: { initialSort: UPDATED_DESC } });
+ } else {
+ mountComponent();
+ }
+
+ findIssuableList().vm.$emit('sort', sortKey);
+ await nextTick();
+
+ expect(findIssuableList().props('urlParams')).toMatchObject({
+ sort: urlSortParams[sortKey],
+ });
+ },
+ );
+
+ describe('when user is signed in', () => {
+ it('calls mutation to save sort preference', () => {
+ const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
+ mountComponent({ sortPreferenceMutationResponse: mutationMock });
+
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
+
+ expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: UPDATED_DESC } });
+ });
+
+ it('captures error when mutation response has errors', async () => {
+ const mutationMock = jest
+ .fn()
+ .mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
+ mountComponent({ sortPreferenceMutationResponse: mutationMock });
+
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
+ await waitForPromises();
+
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!'));
+ });
+ });
+
+ describe('when user is signed out', () => {
+ it('does not call mutation to save sort preference', () => {
+ const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
+ mountComponent({
+ provide: { isSignedIn: false },
+ sortPreferenceMutationResponse: mutationMock,
+ });
+
+ findIssuableList().vm.$emit('sort', CREATED_DESC);
+
+ expect(mutationMock).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/issues/dashboard/mock_data.js b/spec/frontend/issues/dashboard/mock_data.js
new file mode 100644
index 00000000000..feb4cb80bd8
--- /dev/null
+++ b/spec/frontend/issues/dashboard/mock_data.js
@@ -0,0 +1,88 @@
+export const issuesQueryResponse = {
+ data: {
+ issues: {
+ nodes: [
+ {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/123456',
+ iid: '789',
+ closedAt: null,
+ confidential: false,
+ createdAt: '2021-05-22T04:08:01Z',
+ downvotes: 2,
+ dueDate: '2021-05-29',
+ hidden: false,
+ humanTimeEstimate: null,
+ mergeRequestsCount: false,
+ moved: false,
+ reference: 'group/project#123456',
+ state: 'opened',
+ title: 'Issue title',
+ type: 'issue',
+ updatedAt: '2021-05-22T04:08:01Z',
+ upvotes: 3,
+ userDiscussionsCount: 4,
+ webPath: 'project/-/issues/789',
+ webUrl: 'project/-/issues/789',
+ assignees: {
+ nodes: [
+ {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/234',
+ avatarUrl: 'avatar/url',
+ name: 'Marge Simpson',
+ username: 'msimpson',
+ webUrl: 'url/msimpson',
+ },
+ ],
+ },
+ author: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/456',
+ avatarUrl: 'avatar/url',
+ name: 'Homer Simpson',
+ username: 'hsimpson',
+ webUrl: 'url/hsimpson',
+ },
+ labels: {
+ nodes: [
+ {
+ id: 'gid://gitlab/ProjectLabel/456',
+ color: '#333',
+ title: 'Label title',
+ description: 'Label description',
+ },
+ ],
+ },
+ milestone: null,
+ taskCompletionStatus: {
+ completedCount: 1,
+ count: 2,
+ },
+ },
+ ],
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'startcursor',
+ endCursor: 'endcursor',
+ },
+ },
+ },
+};
+
+export const emptyIssuesQueryResponse = {
+ data: {
+ issues: {
+ nodes: [],
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
+ },
+ },
+};
diff --git a/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js
new file mode 100644
index 00000000000..d0d20ef03e1
--- /dev/null
+++ b/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js
@@ -0,0 +1,68 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue';
+import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
+
+describe('EmptyStateWithAnyIssues component', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ emptyStateSvgPath: 'empty/state/svg/path',
+ newIssuePath: 'new/issue/path',
+ showNewIssueLink: false,
+ };
+
+ const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ const mountComponent = (props = {}) => {
+ wrapper = shallowMount(EmptyStateWithAnyIssues, {
+ propsData: {
+ hasSearch: true,
+ isOpenTab: true,
+ ...props,
+ },
+ provide: defaultProvide,
+ });
+ };
+
+ describe('when there is a search (with no results)', () => {
+ beforeEach(() => {
+ mountComponent({ hasSearch: true });
+ });
+
+ it('shows empty state', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ description: IssuesListApp.i18n.noSearchResultsDescription,
+ title: IssuesListApp.i18n.noSearchResultsTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ });
+ });
+ });
+
+ describe('when "Open" tab is active', () => {
+ beforeEach(() => {
+ mountComponent({ hasSearch: false, isOpenTab: true });
+ });
+
+ it('shows empty state', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ description: IssuesListApp.i18n.noOpenIssuesDescription,
+ title: IssuesListApp.i18n.noOpenIssuesTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ });
+ });
+ });
+
+ describe('when "Closed" tab is active', () => {
+ beforeEach(() => {
+ mountComponent({ hasSearch: false, isOpenTab: false });
+ });
+
+ it('shows empty state', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ title: IssuesListApp.i18n.noClosedIssuesTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
new file mode 100644
index 00000000000..065139f10f4
--- /dev/null
+++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
@@ -0,0 +1,211 @@
+import { GlEmptyState, GlLink } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
+import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue';
+import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
+import { i18n } from '~/issues/list/constants';
+
+describe('EmptyStateWithoutAnyIssues component', () => {
+ let wrapper;
+
+ const defaultProps = {
+ currentTabCount: 0,
+ exportCsvPathWithQuery: 'export/csv/path',
+ };
+
+ const defaultProvide = {
+ canCreateProjects: false,
+ emptyStateSvgPath: 'empty/state/svg/path',
+ fullPath: 'full/path',
+ isSignedIn: true,
+ jiraIntegrationPath: 'jira/integration/path',
+ newIssuePath: 'new/issue/path',
+ newProjectPath: 'new/project/path',
+ showNewIssueLink: false,
+ signInPath: 'sign/in/path',
+ };
+
+ const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
+ const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findGlLink = () => wrapper.findComponent(GlLink);
+ const findIssuesHelpPageLink = () =>
+ wrapper.findByRole('link', { name: i18n.noIssuesDescription });
+ const findJiraDocsLink = () =>
+ wrapper.findByRole('link', { name: 'Enable the Jira integration' });
+ const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown);
+ const findNewIssueLink = () => wrapper.findByRole('link', { name: i18n.newIssueLabel });
+ const findNewProjectLink = () => wrapper.findByRole('link', { name: i18n.newProjectLabel });
+
+ const mountComponent = ({ props = {}, provide = {} } = {}) => {
+ wrapper = mountExtended(EmptyStateWithoutAnyIssues, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ stubs: {
+ NewIssueDropdown: true,
+ },
+ });
+ };
+
+ describe('when signed in', () => {
+ describe('empty state', () => {
+ it('renders empty state', () => {
+ mountComponent();
+
+ expect(findGlEmptyState().props()).toMatchObject({
+ title: i18n.noIssuesTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ });
+ });
+
+ describe('description', () => {
+ it('renders issues docs link', () => {
+ mountComponent();
+
+ expect(findIssuesHelpPageLink().attributes('href')).toBe(
+ EmptyStateWithoutAnyIssues.issuesHelpPagePath,
+ );
+ });
+
+ describe('"create a project first" description', () => {
+ describe('when can create projects', () => {
+ it('renders', () => {
+ mountComponent({ provide: { canCreateProjects: true } });
+
+ expect(findGlEmptyState().text()).toContain(i18n.noGroupIssuesSignedInDescription);
+ });
+ });
+
+ describe('when cannot create projects', () => {
+ it('does not render', () => {
+ mountComponent({ provide: { canCreateProjects: false } });
+
+ expect(findGlEmptyState().text()).not.toContain(
+ i18n.noGroupIssuesSignedInDescription,
+ );
+ });
+ });
+ });
+ });
+
+ describe('actions', () => {
+ describe('"New project" link', () => {
+ describe('when can create projects', () => {
+ it('renders', () => {
+ mountComponent({ provide: { canCreateProjects: true } });
+
+ expect(findNewProjectLink().attributes('href')).toBe(defaultProvide.newProjectPath);
+ });
+ });
+
+ describe('when cannot create projects', () => {
+ it('does not render', () => {
+ mountComponent({ provide: { canCreateProjects: false } });
+
+ expect(findNewProjectLink().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('"New issue" link', () => {
+ describe('when can show new issue link', () => {
+ it('renders', () => {
+ mountComponent({ provide: { showNewIssueLink: true } });
+
+ expect(findNewIssueLink().attributes('href')).toBe(defaultProvide.newIssuePath);
+ });
+ });
+
+ describe('when cannot show new issue link', () => {
+ it('does not render', () => {
+ mountComponent({ provide: { showNewIssueLink: false } });
+
+ expect(findNewIssueLink().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('CSV import/export buttons', () => {
+ describe('when can show csv buttons', () => {
+ it('renders', () => {
+ mountComponent({ props: { showCsvButtons: true } });
+
+ expect(findCsvImportExportButtons().props()).toMatchObject({
+ exportCsvPath: defaultProps.exportCsvPathWithQuery,
+ issuableCount: 0,
+ });
+ });
+ });
+
+ describe('when cannot show csv buttons', () => {
+ it('does not render', () => {
+ mountComponent({ props: { showCsvButtons: false } });
+
+ expect(findCsvImportExportButtons().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('new issue dropdown', () => {
+ describe('when can show new issue dropdown', () => {
+ it('renders', () => {
+ mountComponent({ props: { showNewIssueDropdown: true } });
+
+ expect(findNewIssueDropdown().exists()).toBe(true);
+ });
+ });
+
+ describe('when cannot show new issue dropdown', () => {
+ it('does not render', () => {
+ mountComponent({ props: { showNewIssueDropdown: false } });
+
+ expect(findNewIssueDropdown().exists()).toBe(false);
+ });
+ });
+ });
+ });
+ });
+
+ describe('Jira section', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('shows Jira integration information', () => {
+ const paragraphs = wrapper.findAll('p');
+ expect(paragraphs.at(1).text()).toContain(i18n.jiraIntegrationTitle);
+ expect(paragraphs.at(2).text()).toMatchInterpolatedText(i18n.jiraIntegrationMessage);
+ expect(paragraphs.at(3).text()).toContain(i18n.jiraIntegrationSecondaryMessage);
+ });
+
+ it('renders Jira integration docs link', () => {
+ expect(findJiraDocsLink().attributes('href')).toBe(defaultProvide.jiraIntegrationPath);
+ });
+ });
+ });
+
+ describe('when signed out', () => {
+ beforeEach(() => {
+ mountComponent({ provide: { isSignedIn: false } });
+ });
+
+ it('renders empty state', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ title: i18n.noIssuesTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ primaryButtonText: i18n.noIssuesSignedOutButtonText,
+ primaryButtonLink: defaultProvide.signInPath,
+ });
+ });
+
+ it('renders issues docs link', () => {
+ expect(findGlLink().attributes('href')).toBe(EmptyStateWithoutAnyIssues.issuesHelpPagePath);
+ expect(findGlLink().text()).toBe(i18n.noIssuesDescription);
+ });
+ });
+});
diff --git a/spec/frontend/issues/list/components/issue_card_statistics_spec.js b/spec/frontend/issues/list/components/issue_card_statistics_spec.js
new file mode 100644
index 00000000000..180d4ab7eb6
--- /dev/null
+++ b/spec/frontend/issues/list/components/issue_card_statistics_spec.js
@@ -0,0 +1,64 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import IssueCardStatistics from '~/issues/list/components/issue_card_statistics.vue';
+import { i18n } from '~/issues/list/constants';
+
+describe('IssueCardStatistics CE component', () => {
+ let wrapper;
+
+ const findMergeRequests = () => wrapper.findByTestId('merge-requests');
+ const findUpvotes = () => wrapper.findByTestId('issuable-upvotes');
+ const findDownvotes = () => wrapper.findByTestId('issuable-downvotes');
+
+ const mountComponent = ({ mergeRequestsCount, upvotes, downvotes } = {}) => {
+ wrapper = shallowMountExtended(IssueCardStatistics, {
+ propsData: {
+ issue: {
+ mergeRequestsCount,
+ upvotes,
+ downvotes,
+ },
+ },
+ });
+ };
+
+ describe('when issue attributes are undefined', () => {
+ it('does not render the attributes', () => {
+ mountComponent();
+
+ expect(findMergeRequests().exists()).toBe(false);
+ expect(findUpvotes().exists()).toBe(false);
+ expect(findDownvotes().exists()).toBe(false);
+ });
+ });
+
+ describe('when issue attributes are defined', () => {
+ beforeEach(() => {
+ mountComponent({ mergeRequestsCount: 1, upvotes: 5, downvotes: 9 });
+ });
+
+ it('renders merge requests', () => {
+ const mergeRequests = findMergeRequests();
+
+ expect(mergeRequests.text()).toBe('1');
+ expect(mergeRequests.attributes('title')).toBe(i18n.relatedMergeRequests);
+ expect(mergeRequests.findComponent(GlIcon).props('name')).toBe('merge-request');
+ });
+
+ it('renders upvotes', () => {
+ const upvotes = findUpvotes();
+
+ expect(upvotes.text()).toBe('5');
+ expect(upvotes.attributes('title')).toBe(i18n.upvotes);
+ expect(upvotes.findComponent(GlIcon).props('name')).toBe('thumb-up');
+ });
+
+ it('renders downvotes', () => {
+ const downvotes = findDownvotes();
+
+ expect(downvotes.text()).toBe('9');
+ expect(downvotes.attributes('title')).toBe(i18n.downvotes);
+ expect(downvotes.findComponent(GlIcon).props('name')).toBe('thumb-down');
+ });
+ });
+});
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 d0c93c896b3..4c5d8ce3cd1 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -21,18 +21,21 @@ import {
setSortPreferenceMutationResponseWithErrors,
urlParams,
} from 'jest/issues/list/mock_data';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
+import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue';
+import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue';
import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
import {
CREATED_DESC,
RELATIVE_POSITION,
RELATIVE_POSITION_ASC,
+ UPDATED_DESC,
urlSortParams,
} from '~/issues/list/constants';
import eventHub from '~/issues/list/eventhub';
@@ -58,10 +61,11 @@ import {
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_ORGANIZATION,
TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_SEARCH_WITHIN,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
-import('~/issuable/bulk_update_sidebar');
+import('~/issuable');
import('~/users_select');
jest.mock('@sentry/browser');
@@ -122,10 +126,8 @@ describe('CE IssuesListApp component', () => {
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
- const findGlButton = () => wrapper.findComponent(GlButton);
const findGlButtons = () => wrapper.findAllComponents(GlButton);
const findGlButtonAt = (index) => findGlButtons().at(index);
- const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
const findIssuableList = () => wrapper.findComponent(IssuableList);
const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown);
@@ -182,7 +184,11 @@ describe('CE IssuesListApp component', () => {
namespace: defaultProvide.fullPath,
recentSearchesStorageKey: 'issues',
searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder,
- sortOptions: getSortOptions(true, true),
+ sortOptions: getSortOptions({
+ hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
+ }),
initialSortBy: CREATED_DESC,
issuables: getIssuesQueryResponse.data.project.issues.nodes,
tabs: IssuableListTabs,
@@ -395,9 +401,9 @@ describe('CE IssuesListApp component', () => {
});
it('shows an alert to tell the user that manual reordering is disabled', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: IssuesListApp.i18n.issueRepositioningMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
});
});
@@ -435,9 +441,9 @@ describe('CE IssuesListApp component', () => {
});
it('shows an alert to tell the user they must be signed in to search', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: IssuesListApp.i18n.anonymousSearchingMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
});
});
@@ -486,136 +492,29 @@ describe('CE IssuesListApp component', () => {
describe('empty states', () => {
describe('when there are issues', () => {
- describe('when search returns no results', () => {
- beforeEach(() => {
- setWindowLocation(`?search=no+results`);
-
- wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- description: IssuesListApp.i18n.noSearchResultsDescription,
- title: IssuesListApp.i18n.noSearchResultsTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- });
- });
- });
-
- describe('when "Open" tab has no issues', () => {
- beforeEach(() => {
- wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- description: IssuesListApp.i18n.noOpenIssuesDescription,
- title: IssuesListApp.i18n.noOpenIssuesTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- });
- });
+ beforeEach(() => {
+ wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
});
- describe('when "Closed" tab has no issues', () => {
- beforeEach(() => {
- setWindowLocation(`?state=${IssuableStates.Closed}`);
-
- wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- title: IssuesListApp.i18n.noClosedIssuesTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- });
+ it('shows EmptyStateWithAnyIssues empty state', () => {
+ expect(wrapper.findComponent(EmptyStateWithAnyIssues).props()).toEqual({
+ hasSearch: false,
+ isOpenTab: true,
});
});
});
describe('when there are no issues', () => {
- describe('when user is logged in', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- provide: { hasAnyIssues: false, isSignedIn: true },
- mountFn: mount,
- });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- title: IssuesListApp.i18n.noIssuesSignedInTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- });
- expect(findGlEmptyState().text()).toContain(
- IssuesListApp.i18n.noIssuesSignedInDescription,
- );
- });
-
- it('shows "New issue" and import/export buttons', () => {
- expect(findGlButton().text()).toBe(IssuesListApp.i18n.newIssueLabel);
- expect(findGlButton().attributes('href')).toBe(defaultProvide.newIssuePath);
- expect(findCsvImportExportButtons().props()).toMatchObject({
- exportCsvPath: defaultProvide.exportCsvPath,
- issuableCount: 0,
- });
- });
-
- it('shows Jira integration information', () => {
- const paragraphs = wrapper.findAll('p');
- const links = wrapper.findAll('.gl-link');
- expect(paragraphs.at(1).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle);
- expect(paragraphs.at(2).text()).toContain(
- 'Enable the Jira integration to view your Jira issues in GitLab.',
- );
- expect(paragraphs.at(3).text()).toContain(
- IssuesListApp.i18n.jiraIntegrationSecondaryMessage,
- );
- expect(links.at(1).text()).toBe('Enable the Jira integration');
- expect(links.at(1).attributes('href')).toBe(defaultProvide.jiraIntegrationPath);
- });
- });
-
- describe('when user is logged in and can create projects', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- provide: { canCreateProjects: true, hasAnyIssues: false, isSignedIn: true },
- stubs: { GlEmptyState },
- });
- });
-
- it('shows empty state with additional description about creating projects', () => {
- expect(findGlEmptyState().text()).toContain(
- IssuesListApp.i18n.noIssuesSignedInDescription,
- );
- expect(findGlEmptyState().text()).toContain(
- IssuesListApp.i18n.noGroupIssuesSignedInDescription,
- );
- });
-
- it('shows "New project" button', () => {
- expect(findGlButton().text()).toBe(IssuesListApp.i18n.newProjectLabel);
- expect(findGlButton().attributes('href')).toBe(defaultProvide.newProjectPath);
- });
+ beforeEach(() => {
+ wrapper = mountComponent({ provide: { hasAnyIssues: false } });
});
- describe('when user is logged out', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- provide: { hasAnyIssues: false, isSignedIn: false },
- mountFn: mount,
- });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- title: IssuesListApp.i18n.noIssuesSignedOutTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- primaryButtonText: IssuesListApp.i18n.noIssuesSignedOutButtonText,
- primaryButtonLink: defaultProvide.signInPath,
- });
- expect(findGlEmptyState().text()).toContain(
- IssuesListApp.i18n.noIssuesSignedOutDescription,
- );
+ it('shows EmptyStateWithoutAnyIssues empty state', () => {
+ expect(wrapper.findComponent(EmptyStateWithoutAnyIssues).props()).toEqual({
+ currentTabCount: 0,
+ exportCsvPathWithQuery: defaultProvide.exportCsvPath,
+ showCsvButtons: true,
+ showNewIssueDropdown: false,
});
});
});
@@ -636,8 +535,8 @@ describe('CE IssuesListApp component', () => {
it('does not render My-Reaction or Confidential tokens', () => {
expect(findIssuableList().props('searchTokens')).not.toMatchObject([
- { type: TOKEN_TYPE_AUTHOR, preloadedAuthors: [mockCurrentUser] },
- { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors: [mockCurrentUser] },
+ { type: TOKEN_TYPE_AUTHOR, preloadedUsers: [mockCurrentUser] },
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers: [mockCurrentUser] },
{ type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_CONFIDENTIAL },
]);
@@ -685,13 +584,13 @@ describe('CE IssuesListApp component', () => {
});
it('renders all tokens alphabetically', () => {
- const preloadedAuthors = [
+ const preloadedUsers = [
{ ...mockCurrentUser, id: convertToGraphQLId('User', mockCurrentUser.id) },
];
expect(findIssuableList().props('searchTokens')).toMatchObject([
- { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors },
- { type: TOKEN_TYPE_AUTHOR, preloadedAuthors },
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
+ { type: TOKEN_TYPE_AUTHOR, preloadedUsers },
{ type: TOKEN_TYPE_CONFIDENTIAL },
{ type: TOKEN_TYPE_CONTACT },
{ type: TOKEN_TYPE_LABEL },
@@ -699,6 +598,7 @@ describe('CE IssuesListApp component', () => {
{ type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_ORGANIZATION },
{ type: TOKEN_TYPE_RELEASE },
+ { type: TOKEN_TYPE_SEARCH_WITHIN },
{ type: TOKEN_TYPE_TYPE },
]);
});
@@ -899,7 +799,11 @@ describe('CE IssuesListApp component', () => {
it.each(Object.keys(urlSortParams))(
'updates to the new sort when payload is `%s`',
async (sortKey) => {
- wrapper = mountComponent();
+ // Ensure initial sort key is different so we can trigger an update when emitting a sort key
+ wrapper =
+ sortKey === CREATED_DESC
+ ? mountComponent({ provide: { initialSort: UPDATED_DESC } })
+ : mountComponent();
router.push = jest.fn();
findIssuableList().vm.$emit('sort', sortKey);
@@ -929,9 +833,9 @@ describe('CE IssuesListApp component', () => {
});
it('shows an alert to tell the user that manual reordering is disabled', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: IssuesListApp.i18n.issueRepositioningMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
});
});
@@ -941,9 +845,9 @@ describe('CE IssuesListApp component', () => {
const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
wrapper = mountComponent({ sortPreferenceMutationResponse: mutationMock });
- findIssuableList().vm.$emit('sort', CREATED_DESC);
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
- expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: CREATED_DESC } });
+ expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: UPDATED_DESC } });
});
it('captures error when mutation response has errors', async () => {
@@ -952,7 +856,7 @@ describe('CE IssuesListApp component', () => {
.mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
wrapper = mountComponent({ sortPreferenceMutationResponse: mutationMock });
- findIssuableList().vm.$emit('sort', CREATED_DESC);
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
await waitForPromises();
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!'));
@@ -1016,9 +920,9 @@ describe('CE IssuesListApp component', () => {
});
it('shows an alert to tell the user they must be signed in to search', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: IssuesListApp.i18n.anonymousSearchingMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
});
});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 62fcbf7aad0..0690501dee9 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -1,7 +1,7 @@
import {
FILTERED_SEARCH_TERM,
OPERATOR_IS,
- OPERATOR_IS_NOT,
+ OPERATOR_NOT,
OPERATOR_OR,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
@@ -132,6 +132,8 @@ export const locationSearch = [
'?search=find+issues',
'author_username=homer',
'not[author_username]=marge',
+ 'or[author_username]=burns',
+ 'or[author_username]=smithers',
'assignee_username[]=bart',
'assignee_username[]=lisa',
'assignee_username[]=5',
@@ -184,41 +186,43 @@ export const locationSearchWithSpecialValues = [
export const filteredTokens = [
{ type: TOKEN_TYPE_AUTHOR, value: { data: 'homer', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_AUTHOR, value: { data: 'marge', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'marge', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'burns', operator: OPERATOR_OR } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'smithers', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'bart', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lisa', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: '5', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'patty', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'selma', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'patty', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'selma', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'carl', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lenny', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 4', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 20', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 30', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 20', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 30', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'cartoon', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'tv', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v4', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_RELEASE, value: { data: 'v30', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'v30', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_TYPE, value: { data: 'issue', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_TYPE, value: { data: 'feature', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_TYPE, value: { data: 'bug', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_TYPE, value: { data: 'incident', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_TYPE, value: { data: 'bug', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_TYPE, value: { data: 'incident', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsup', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONFIDENTIAL, value: { data: 'yes', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '12', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_ITERATION, value: { data: '20', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_ITERATION, value: { data: '42', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: '20', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: '42', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_EPIC, value: { data: '12', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_EPIC, value: { data: '34', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_EPIC, value: { data: '34', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_WEIGHT, value: { data: '1', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONTACT, value: { data: '123', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ORGANIZATION, value: { data: '456', operator: OPERATOR_IS } },
{ type: FILTERED_SEARCH_TERM, value: { data: 'find' } },
@@ -264,6 +268,7 @@ export const apiParams = {
weight: '3',
},
or: {
+ authorUsernames: ['burns', 'smithers'],
assigneeUsernames: ['carl', 'lenny'],
},
};
@@ -283,6 +288,7 @@ export const apiParamsWithSpecialValues = {
export const urlParams = {
author_username: 'homer',
'not[author_username]': 'marge',
+ 'or[author_username]': ['burns', 'smithers'],
'assignee_username[]': ['bart', 'lisa', '5'],
'not[assignee_username][]': ['patty', 'selma'],
'or[assignee_username][]': ['carl', 'lenny'],
diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
index 3c6332d5728..a281ed1c989 100644
--- a/spec/frontend/issues/list/utils_spec.js
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -69,26 +69,40 @@ describe('isSortKey', () => {
describe('getSortOptions', () => {
describe.each`
- hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsWeight | containsBlocking
- ${false} | ${false} | ${10} | ${false} | ${false}
- ${true} | ${false} | ${11} | ${true} | ${false}
- ${false} | ${true} | ${11} | ${false} | ${true}
- ${true} | ${true} | ${12} | ${true} | ${true}
+ hasIssuableHealthStatusFeature | hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsHealthStatus | containsWeight | containsBlocking
+ ${false} | ${false} | ${false} | ${10} | ${false} | ${false} | ${false}
+ ${false} | ${false} | ${true} | ${11} | ${false} | ${false} | ${true}
+ ${false} | ${true} | ${false} | ${11} | ${false} | ${true} | ${false}
+ ${false} | ${true} | ${true} | ${12} | ${false} | ${true} | ${true}
+ ${true} | ${false} | ${false} | ${11} | ${true} | ${false} | ${false}
+ ${true} | ${false} | ${true} | ${12} | ${true} | ${false} | ${true}
+ ${true} | ${true} | ${false} | ${12} | ${true} | ${true} | ${false}
+ ${true} | ${true} | ${true} | ${13} | ${true} | ${true} | ${true}
`(
- 'when hasIssueWeightsFeature=$hasIssueWeightsFeature and hasBlockedIssuesFeature=$hasBlockedIssuesFeature',
+ 'when hasIssuableHealthStatusFeature=$hasIssuableHealthStatusFeature, hasIssueWeightsFeature=$hasIssueWeightsFeature and hasBlockedIssuesFeature=$hasBlockedIssuesFeature',
({
+ hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
hasBlockedIssuesFeature,
length,
+ containsHealthStatus,
containsWeight,
containsBlocking,
}) => {
- const sortOptions = getSortOptions(hasIssueWeightsFeature, hasBlockedIssuesFeature);
+ const sortOptions = getSortOptions({
+ hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature,
+ });
it('returns the correct length of sort options', () => {
expect(sortOptions).toHaveLength(length);
});
+ it(`${containsHealthStatus ? 'contains' : 'does not contain'} health status option`, () => {
+ expect(sortOptions.some((option) => option.title === 'Health')).toBe(containsHealthStatus);
+ });
+
it(`${containsWeight ? 'contains' : 'does not contain'} weight option`, () => {
expect(sortOptions.some((option) => option.title === 'Weight')).toBe(containsWeight);
});
diff --git a/spec/frontend/issues/related_merge_requests/store/actions_spec.js b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
index 4327fac15d4..d3ec6c3bc9d 100644
--- a/spec/frontend/issues/related_merge_requests/store/actions_spec.js
+++ b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/issues/related_merge_requests/store/actions';
import * as types from '~/issues/related_merge_requests/store/mutation_types';
@@ -95,8 +95,8 @@ describe('RelatedMergeRequest store actions', () => {
[],
[{ type: 'requestData' }, { type: 'receiveDataError' }],
);
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: expect.stringMatching('Something went wrong'),
});
});
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 3d027e2084c..6cf44e60092 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -5,7 +5,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import '~/behaviors/markdown/render_gfm';
+import { createAlert } from '~/flash';
import { IssuableStatus, IssuableStatusText, IssuableType } from '~/issues/constants';
import IssuableApp from '~/issues/show/components/app.vue';
import DescriptionComponent from '~/issues/show/components/description.vue';
@@ -26,8 +26,10 @@ import {
zoomMeetingUrl,
} from '../mock_data/mock_data';
-jest.mock('~/lib/utils/url_utility');
+jest.mock('~/flash');
jest.mock('~/issues/show/event_hub');
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/behaviors/markdown/render_gfm');
const REALTIME_REQUEST_STACK = [initialRequest, secondRequest];
@@ -270,9 +272,7 @@ describe('Issuable output', () => {
await wrapper.vm.updateIssuable();
expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `Error updating issue`,
- );
+ expect(createAlert).toHaveBeenCalledWith({ message: `Error updating issue` });
});
it('returns the correct error message for issuableType', async () => {
@@ -282,9 +282,7 @@ describe('Issuable output', () => {
await nextTick();
await wrapper.vm.updateIssuable();
expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `Error updating merge request`,
- );
+ expect(createAlert).toHaveBeenCalledWith({ message: `Error updating merge request` });
});
it('shows error message from backend if exists', async () => {
@@ -294,9 +292,9 @@ describe('Issuable output', () => {
.mockRejectedValue({ response: { data: { errors: [msg] } } });
await wrapper.vm.updateIssuable();
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `${wrapper.vm.defaultErrorMessage}. ${msg}`,
- );
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `${wrapper.vm.defaultErrorMessage}. ${msg}`,
+ });
});
});
});
@@ -354,9 +352,7 @@ describe('Issuable output', () => {
.reply(() => Promise.reject(new Error('something went wrong')));
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
- expect(document.querySelector('.flash-container .flash-text').textContent).toContain(
- 'Error updating issue',
- );
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Error updating issue' });
expect(formSpy).toHaveBeenCalledWith();
});
@@ -402,9 +398,9 @@ describe('Issuable output', () => {
wrapper.setProps({ issuableType: 'merge request' });
return wrapper.vm.updateStoreState().then(() => {
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `Error updating ${wrapper.vm.issuableType}`,
- );
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Error updating ${wrapper.vm.issuableType}`,
+ });
});
});
});
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 9d9abce887b..889ff450825 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import '~/behaviors/markdown/render_gfm';
import { GlTooltip, GlModal } from '@gitlab/ui';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -12,7 +11,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Description from '~/issues/show/components/description.vue';
import { updateHistory } from '~/lib/utils/url_utility';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@@ -21,6 +20,7 @@ import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_ite
import TaskList from '~/task_list';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import {
projectWorkItemTypesQueryResponse,
createWorkItemFromTaskMutationResponse,
@@ -37,6 +37,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
updateHistory: jest.fn(),
}));
jest.mock('~/task_list');
+jest.mock('~/behaviors/markdown/render_gfm');
const showModal = jest.fn();
const hideModal = jest.fn();
@@ -161,7 +162,6 @@ describe('Description component', () => {
});
it('applies syntax highlighting and math when description changed', async () => {
- const prototypeSpy = jest.spyOn($.prototype, 'renderGFM');
createComponent();
await wrapper.setProps({
@@ -169,7 +169,7 @@ describe('Description component', () => {
});
expect(findGfmContent().exists()).toBe(true);
- expect(prototypeSpy).toHaveBeenCalled();
+ expect(renderGFM).toHaveBeenCalled();
});
it('sets data-update-url', () => {
@@ -370,7 +370,7 @@ describe('Description component', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith(
+ expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Something went wrong when creating task. Please try again.',
}),
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index dc2b3c6fc48..7d6ca44e679 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -3,7 +3,7 @@ import { GlButton, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { IssuableStatus, IssueType } from '~/issues/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
@@ -171,19 +171,19 @@ describe('HeaderActions component', () => {
${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems} | ${findDesktopDropdown}
`('$description', ({ isCloseIssueItemVisible, findDropdownItems, findDropdown }) => {
describe.each`
- description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic | canDestroyIssue
- ${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${`when user can create ${issueType}`} | ${`New related ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${`when user cannot create ${issueType}`} | ${`New related ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true} | ${true}
- ${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false} | ${true}
- ${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true} | ${true}
- ${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
- ${`when user can delete ${issueType}`} | ${`Delete ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${`when user cannot delete ${issueType}`} | ${`Delete ${issueType}`} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${false}
+ description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic | canDestroyIssue
+ ${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user can create ${issueType}`} | ${`New related ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user cannot create ${issueType}`} | ${`New related ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true} | ${true}
+ ${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false} | ${true}
+ ${'when user can report abuse'} | ${'Report abuse to administrator'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true} | ${true}
+ ${'when user cannot report abuse'} | ${'Report abuse to administrator'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
+ ${`when user can delete ${issueType}`} | ${`Delete ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user cannot delete ${issueType}`} | ${`Delete ${issueType}`} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${false}
`(
'$description',
({
@@ -284,9 +284,9 @@ describe('HeaderActions component', () => {
});
it('shows a success message and tells the user they are being redirected', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'The issue was successfully promoted to an epic. Redirecting to epic...',
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
});
@@ -309,7 +309,7 @@ describe('HeaderActions component', () => {
});
it('shows an error message', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: HeaderActions.i18n.promoteErrorMessage,
});
});
diff --git a/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js b/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js
index 4c1638a9147..81c3c30bf8a 100644
--- a/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js
+++ b/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js
@@ -40,5 +40,13 @@ describe('Edit Timeline events', () => {
expect(wrapper.emitted()).toEqual(cancelEvent);
});
+
+ it('should emit the delete event', async () => {
+ const deleteEvent = { delete: [[]] };
+
+ await findTimelineEventsForm().vm.$emit('delete');
+
+ expect(wrapper.emitted()).toEqual(deleteEvent);
+ });
});
});
diff --git a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
index 458c1c3f858..33a3a6eddfc 100644
--- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
@@ -1,10 +1,11 @@
-import { GlTab } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import merge from 'lodash/merge';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
import DescriptionComponent from '~/issues/show/components/description.vue';
import HighlightBar from '~/issues/show/components/incidents/highlight_bar.vue';
-import IncidentTabs from '~/issues/show/components/incidents/incident_tabs.vue';
+import IncidentTabs, {
+ incidentTabsI18n,
+} from '~/issues/show/components/incidents/incident_tabs.vue';
import INVALID_URL from '~/lib/utils/invalid_url';
import Tracking from '~/tracking';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
@@ -16,11 +17,24 @@ const mockAlert = {
iid: '1',
};
+const defaultMocks = {
+ $apollo: {
+ queries: {
+ alert: {
+ loading: true,
+ },
+ timelineEvents: {
+ loading: false,
+ },
+ },
+ },
+};
+
describe('Incident Tabs component', () => {
let wrapper;
- const mountComponent = (data = {}, options = {}) => {
- wrapper = shallowMount(
+ const mountComponent = ({ data = {}, options = {}, mount = shallowMountExtended } = {}) => {
+ wrapper = mount(
IncidentTabs,
merge(
{
@@ -29,7 +43,7 @@ describe('Incident Tabs component', () => {
},
stubs: {
DescriptionComponent: true,
- MetricsTab: true,
+ IncidentMetricTab: true,
},
provide: {
fullPath: '',
@@ -37,41 +51,37 @@ describe('Incident Tabs component', () => {
projectId: '',
issuableId: '',
uploadMetricsFeatureAvailable: true,
+ slaFeatureAvailable: true,
+ canUpdate: true,
+ canUpdateTimelineEvent: true,
},
data() {
return { alert: mockAlert, ...data };
},
- mocks: {
- $apollo: {
- queries: {
- alert: {
- loading: true,
- },
- timelineEvents: {
- loading: false,
- },
- },
- },
- },
+ mocks: defaultMocks,
},
options,
),
);
};
- const findTabs = () => wrapper.findAllComponents(GlTab);
- const findSummaryTab = () => findTabs().at(0);
- const findAlertDetailsTab = () => wrapper.find('[data-testid="alert-details-tab"]');
+ const findSummaryTab = () => wrapper.findByTestId('summary-tab');
+ const findTimelineTab = () => wrapper.findByTestId('timeline-tab');
+ const findAlertDetailsTab = () => wrapper.findByTestId('alert-details-tab');
const findAlertDetailsComponent = () => wrapper.findComponent(AlertDetailsTable);
const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent);
const findHighlightBarComponent = () => wrapper.findComponent(HighlightBar);
+ const findTabButtonByFilter = (filter) => wrapper.findAllByRole('tab').filter(filter);
+ const findTimelineTabButton = () =>
+ findTabButtonByFilter((inner) => inner.text() === incidentTabsI18n.timelineTitle).at(0);
+ const findActiveTabs = () => findTabButtonByFilter((inner) => inner.classes('active'));
- describe('empty state', () => {
+ describe('with no alerts', () => {
beforeEach(() => {
- mountComponent({ alert: null });
+ mountComponent({ data: { alert: null } });
});
- it('does not show the alert details tab', () => {
+ it('does not show the alert details tab option', () => {
expect(findAlertDetailsComponent().exists()).toBe(false);
});
});
@@ -83,7 +93,12 @@ describe('Incident Tabs component', () => {
it('renders the summary tab', () => {
expect(findSummaryTab().exists()).toBe(true);
- expect(findSummaryTab().attributes('title')).toBe('Summary');
+ expect(findSummaryTab().attributes('title')).toBe(incidentTabsI18n.summaryTitle);
+ });
+
+ it('renders the timeline tab', () => {
+ expect(findTimelineTab().exists()).toBe(true);
+ expect(findTimelineTab().attributes('title')).toBe(incidentTabsI18n.timelineTitle);
});
it('renders the alert details tab', () => {
@@ -125,4 +140,22 @@ describe('Incident Tabs component', () => {
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
});
+
+ describe('tab changing', () => {
+ beforeEach(() => {
+ mountComponent({ mount: mountExtended });
+ });
+
+ it('shows only the summary tab by default', async () => {
+ expect(findActiveTabs()).toHaveLength(1);
+ expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.summaryTitle);
+ });
+
+ it("shows the timeline tab after it's clicked", async () => {
+ await findTimelineTabButton().trigger('click');
+
+ expect(findActiveTabs()).toHaveLength(1);
+ expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.timelineTitle);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js
index adea2b6df59..9accfcea791 100644
--- a/spec/frontend/issues/show/components/incidents/mock_data.js
+++ b/spec/frontend/issues/show/components/incidents/mock_data.js
@@ -13,6 +13,9 @@ export const mockEvents = [
noteHtml: '<p>Dummy event 1</p>',
occurredAt: '2022-03-22T15:59:00Z',
updatedAt: '2022-03-22T15:59:08Z',
+ timelineEventTags: {
+ nodes: [],
+ },
__typename: 'TimelineEventType',
},
{
@@ -29,6 +32,18 @@ export const mockEvents = [
noteHtml: '<p>Dummy event 2</p>',
occurredAt: '2022-03-23T14:57:00Z',
updatedAt: '2022-03-23T14:57:08Z',
+ timelineEventTags: {
+ nodes: [
+ {
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
+ name: 'Start time',
+ },
+ {
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
+ name: 'End time',
+ },
+ ],
+ },
__typename: 'TimelineEventType',
},
{
@@ -45,6 +60,9 @@ export const mockEvents = [
noteHtml: '<p>Dummy event 3</p>',
occurredAt: '2022-03-23T15:59:00Z',
updatedAt: '2022-03-23T15:59:08Z',
+ timelineEventTags: {
+ nodes: [],
+ },
__typename: 'TimelineEventType',
},
];
@@ -152,6 +170,9 @@ export const mockGetTimelineData = {
action: 'comment',
occurredAt: '2022-07-01T12:47:00Z',
createdAt: '2022-07-20T12:47:40Z',
+ timelineEventTags: {
+ nodes: [],
+ },
},
],
},
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
index 0ce3f75f576..d5b199cc790 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
@@ -22,11 +22,12 @@ describe('Timeline events form', () => {
useFakeDate(fakeDate);
let wrapper;
- const mountComponent = ({ mountMethod = shallowMountExtended } = {}) => {
+ const mountComponent = ({ mountMethod = shallowMountExtended } = {}, props = {}) => {
wrapper = mountMethod(TimelineEventsForm, {
propsData: {
showSaveAndAdd: true,
isEventProcessed: false,
+ ...props,
},
stubs: {
GlButton: true,
@@ -43,6 +44,7 @@ describe('Timeline events form', () => {
const findSubmitButton = () => wrapper.findByText(timelineFormI18n.save);
const findSubmitAndAddButton = () => wrapper.findByText(timelineFormI18n.saveAndAdd);
const findCancelButton = () => wrapper.findByText(timelineFormI18n.cancel);
+ const findDeleteButton = () => wrapper.findByText(timelineFormI18n.delete);
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
const findHourInput = () => wrapper.findByTestId('input-hours');
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
@@ -68,6 +70,9 @@ describe('Timeline events form', () => {
findCancelButton().vm.$emit('click');
await waitForPromises();
};
+ const deleteForm = () => {
+ findDeleteButton().vm.$emit('click');
+ };
it('renders markdown-field component with correct list of toolbar items', () => {
mountComponent({ mountMethod: mountExtended });
@@ -165,4 +170,38 @@ describe('Timeline events form', () => {
expect(findSubmitAndAddButton().props('disabled')).toBe(true);
});
});
+
+ describe('Delete button', () => {
+ it('does not show the delete button if showDelete prop is false', () => {
+ mountComponent({ mountMethod: mountExtended }, { showDelete: false });
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
+ it('shows the delete button if showDelete prop is true', () => {
+ mountComponent({ mountMethod: mountExtended }, { showDelete: true });
+
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ it('disables the delete button if isEventProcessed prop is true', () => {
+ mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: true });
+
+ expect(findDeleteButton().props('disabled')).toBe(true);
+ });
+
+ it('does not disable the delete button if isEventProcessed prop is false', () => {
+ mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: false });
+
+ expect(findDeleteButton().props('disabled')).toBe(false);
+ });
+
+ it('emits delete event on click', () => {
+ mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: true });
+
+ deleteForm();
+
+ expect(wrapper.emitted('delete')).toEqual([[]]);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
index 1bf8d68efd4..ba0527e5395 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
@@ -1,5 +1,5 @@
import timezoneMock from 'timezone-mock';
-import { GlIcon, GlDropdown } from '@gitlab/ui';
+import { GlIcon, GlDropdown, GlBadge } from '@gitlab/ui';
import { nextTick } from 'vue';
import { timelineItemI18n } from '~/issues/show/components/incidents/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -27,25 +27,24 @@ describe('IncidentTimelineEventList', () => {
const findCommentIcon = () => wrapper.findComponent(GlIcon);
const findEventTime = () => wrapper.findByTestId('event-time');
+ const findEventTag = () => wrapper.findComponent(GlBadge);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDeleteButton = () => wrapper.findByText(timelineItemI18n.delete);
describe('template', () => {
- it('shows comment icon', () => {
+ beforeEach(() => {
mountComponent();
+ });
+ it('shows comment icon', () => {
expect(findCommentIcon().exists()).toBe(true);
});
it('sets correct props for icon', () => {
- mountComponent();
-
expect(findCommentIcon().props('name')).toBe(mockEvents[0].action);
});
it('displays the correct time', () => {
- mountComponent();
-
expect(findEventTime().text()).toBe('15:59 UTC');
});
@@ -58,8 +57,6 @@ describe('IncidentTimelineEventList', () => {
describe(timezone, () => {
beforeEach(() => {
timezoneMock.register(timezone);
-
- mountComponent();
});
afterEach(() => {
@@ -72,10 +69,20 @@ describe('IncidentTimelineEventList', () => {
});
});
+ describe('timeline event tag', () => {
+ it('does not show when tag is not provided', () => {
+ expect(findEventTag().exists()).toBe(false);
+ });
+
+ it('shows when tag is provided', () => {
+ mountComponent({ propsData: { eventTag: 'Start time' } });
+
+ expect(findEventTag().exists()).toBe(true);
+ });
+ });
+
describe('action dropdown', () => {
it('does not show the action dropdown by default', () => {
- mountComponent();
-
expect(findDropdown().exists()).toBe(false);
expect(findDeleteButton().exists()).toBe(false);
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
index dff1c429d07..a7250e8ad0d 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
@@ -92,6 +92,9 @@ describe('IncidentTimelineEventList', () => {
expect(findItems().at(1).props('occurredAt')).toBe(mockEvents[1].occurredAt);
expect(findItems().at(1).props('action')).toBe(mockEvents[1].action);
expect(findItems().at(1).props('noteHtml')).toBe(mockEvents[1].noteHtml);
+ expect(findItems().at(1).props('eventTag')).toBe(
+ mockEvents[1].timelineEventTags.nodes[0].name,
+ );
});
it('formats dates correctly', () => {
@@ -120,6 +123,20 @@ describe('IncidentTimelineEventList', () => {
});
});
+ describe('getFirstTag', () => {
+ it('returns undefined, when timelineEventTags contains an empty array', () => {
+ const returnedTag = wrapper.vm.getFirstTag(mockEvents[0].timelineEventTags);
+
+ expect(returnedTag).toEqual(undefined);
+ });
+
+ it('returns the first string, when timelineEventTags contains array with at least one tag', () => {
+ const returnedTag = wrapper.vm.getFirstTag(mockEvents[1].timelineEventTags);
+
+ expect(returnedTag).toBe(mockEvents[1].timelineEventTags.nodes[0].name);
+ });
+ });
+
describe('delete functionality', () => {
beforeEach(() => {
mockConfirmAction({ confirmed: true });
diff --git a/spec/frontend/issues/show/components/locked_warning_spec.js b/spec/frontend/issues/show/components/locked_warning_spec.js
new file mode 100644
index 00000000000..08f0338d41b
--- /dev/null
+++ b/spec/frontend/issues/show/components/locked_warning_spec.js
@@ -0,0 +1,55 @@
+import { GlAlert, GlLink } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { sprintf } from '~/locale';
+import { IssuableType } from '~/issues/constants';
+import LockedWarning, { i18n } from '~/issues/show/components/locked_warning.vue';
+
+describe('LockedWarning component', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mountExtended(LockedWarning, {
+ propsData: props,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ describe.each([IssuableType.Issue, IssuableType.Epic])(
+ 'with issuableType set to %s',
+ (issuableType) => {
+ let alert;
+ let link;
+ beforeEach(() => {
+ createComponent({ issuableType });
+ alert = findAlert();
+ link = findLink();
+ });
+
+ afterEach(() => {
+ alert = null;
+ link = null;
+ });
+
+ it('displays a non-closable alert', () => {
+ expect(alert.exists()).toBe(true);
+ expect(alert.props('dismissible')).toBe(false);
+ });
+
+ it(`displays correct message`, async () => {
+ expect(alert.text()).toMatchInterpolatedText(sprintf(i18n.alertMessage, { issuableType }));
+ });
+
+ it(`displays a link with correct text`, async () => {
+ expect(link.exists()).toBe(true);
+ expect(link.text()).toBe(`the ${issuableType}`);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
index 56eb6d75def..56e425fa4eb 100644
--- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -29,19 +29,12 @@ const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
describe('SourceBranchDropdown', () => {
let wrapper;
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findDropdownItemByText = (text) =>
- findAllDropdownItems().wrappers.find((item) => item.text() === text);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
-
- const assertDropdownItems = () => {
- const dropdownItems = findAllDropdownItems();
- expect(dropdownItems.wrappers).toHaveLength(mockProject.repository.branchNames.length);
- expect(dropdownItems.wrappers.map((item) => item.text())).toEqual(
- mockProject.repository.branchNames,
- );
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ const assertListboxItems = () => {
+ const listboxItems = findListbox().props('items');
+ expect(listboxItems).toHaveLength(mockProject.repository.branchNames.length);
+ expect(listboxItems.map((item) => item.text)).toEqual(mockProject.repository.branchNames);
};
function createMockApolloProvider({ getProjectQueryLoading = false } = {}) {
@@ -70,8 +63,8 @@ describe('SourceBranchDropdown', () => {
createComponent();
});
- it('sets dropdown `disabled` prop to `true`', () => {
- expect(findDropdown().props('disabled')).toBe(true);
+ it('sets listbox `disabled` prop to `true`', () => {
+ expect(findListbox().props('disabled')).toBe(true);
});
describe('when `selectedProject` becomes specified', () => {
@@ -83,29 +76,30 @@ describe('SourceBranchDropdown', () => {
await waitForPromises();
});
- it('sets dropdown props correctly', () => {
- expect(findDropdown().props()).toMatchObject({
- loading: false,
+ it('sets listbox props correctly', () => {
+ expect(findListbox().props()).toMatchObject({
disabled: false,
- text: 'Select a branch',
+ loading: false,
+ searchable: true,
+ searching: false,
+ toggleText: 'Select a branch',
});
});
- it('renders available source branches as dropdown items', () => {
- assertDropdownItems();
+ it('renders available source branches as listbox items', () => {
+ assertListboxItems();
});
});
});
describe('when `selectedProject` prop is specified', () => {
describe('when branches are loading', () => {
- it('renders loading icon in dropdown', () => {
+ it('sets loading prop to true', () => {
createComponent({
mockApollo: createMockApolloProvider({ getProjectQueryLoading: true }),
props: { selectedProject: mockSelectedProject },
});
-
- expect(findLoadingIcon().isVisible()).toBe(true);
+ expect(findListbox().props('loading')).toEqual(true);
});
});
@@ -117,7 +111,7 @@ describe('SourceBranchDropdown', () => {
jest.clearAllMocks();
const mockSearchTerm = 'mai';
- await findSearchBox().vm.$emit('input', mockSearchTerm);
+ await findListbox().vm.$emit('search', mockSearchTerm);
expect(mockGetProjectQuery).toHaveBeenCalledWith({
branchNamesLimit: BRANCHES_PER_PAGE,
@@ -134,32 +128,32 @@ describe('SourceBranchDropdown', () => {
await waitForPromises();
});
- it('sets dropdown props correctly', () => {
- expect(findDropdown().props()).toMatchObject({
- loading: false,
+ it('sets listbox props correctly', () => {
+ expect(findListbox().props()).toMatchObject({
disabled: false,
- text: 'Select a branch',
+ loading: false,
+ searchable: true,
+ searching: false,
+ toggleText: 'Select a branch',
});
});
- it('omits monospace styling from dropdown', () => {
- expect(findDropdown().classes()).not.toContain('gl-font-monospace');
+ it('omits monospace styling from listbox', () => {
+ expect(findListbox().classes()).not.toContain('gl-font-monospace');
});
- it('renders available source branches as dropdown items', () => {
- assertDropdownItems();
+ it('renders available source branches as listbox items', () => {
+ assertListboxItems();
});
it("emits `change` event with the repository's `rootRef` by default", () => {
expect(wrapper.emitted('change')[0]).toEqual([mockProject.repository.rootRef]);
});
- describe('when selecting a dropdown item', () => {
+ describe('when selecting a listbox item', () => {
it('emits `change` event with the selected branch name', async () => {
const mockBranchName = mockProject.repository.branchNames[1];
- const itemToSelect = findDropdownItemByText(mockBranchName);
- await itemToSelect.vm.$emit('click');
-
+ findListbox().vm.$emit('select', mockBranchName);
expect(wrapper.emitted('change')[1]).toEqual([mockBranchName]);
});
});
@@ -173,16 +167,12 @@ describe('SourceBranchDropdown', () => {
});
});
- it('sets `isChecked` prop of the corresponding dropdown item to `true`', () => {
- expect(findDropdownItemByText(mockBranchName).props('isChecked')).toBe(true);
- });
-
- it('sets dropdown text to `selectedBranchName` value', () => {
- expect(findDropdown().props('text')).toBe(mockBranchName);
+ it('sets listbox text to `selectedBranchName` value', () => {
+ expect(findListbox().props('toggleText')).toBe(mockBranchName);
});
- it('adds monospace styling to dropdown', () => {
- expect(findDropdown().classes()).toContain('gl-font-monospace');
+ it('adds monospace styling to listbox', () => {
+ expect(findListbox().classes()).toContain('gl-font-monospace');
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
index 10696d25f17..e98c6ff1054 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
@@ -1,12 +1,14 @@
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import SetupInstructions from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue';
import SignInGitlabMultiversion from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue';
-import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue';
import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
+import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue';
import { updateInstallation } from '~/jira_connect/subscriptions/api';
import { reloadPage, persistBaseUrl, retrieveBaseUrl } from '~/jira_connect/subscriptions/utils';
+import { GITLAB_COM_BASE_PATH } from '~/jira_connect/subscriptions/constants';
jest.mock('~/jira_connect/subscriptions/api', () => {
return {
@@ -21,8 +23,9 @@ describe('SignInGitlabMultiversion', () => {
const mockBasePath = 'gitlab.mycompany.com';
- const findVersionSelectForm = () => wrapper.findComponent(VersionSelectForm);
+ const findSetupInstructions = () => wrapper.findComponent(SetupInstructions);
const findSignInOauthButton = () => wrapper.findComponent(SignInOauthButton);
+ const findVersionSelectForm = () => wrapper.findComponent(VersionSelectForm);
const findSubtitle = () => wrapper.findByTestId('subtitle');
const createComponent = () => {
@@ -59,15 +62,48 @@ describe('SignInGitlabMultiversion', () => {
});
describe('when version is selected', () => {
- beforeEach(() => {
- retrieveBaseUrl.mockReturnValue(mockBasePath);
- createComponent();
+ describe('when on self-managed', () => {
+ beforeEach(() => {
+ retrieveBaseUrl.mockReturnValue(mockBasePath);
+ createComponent();
+ });
+
+ it('renders correct subtitle', () => {
+ expect(findSubtitle().text()).toBe(SignInGitlabMultiversion.i18n.signInSubtitle);
+ });
+
+ it('renders setup instructions', () => {
+ expect(findSetupInstructions().exists()).toBe(true);
+ });
+
+ describe('when SetupInstructions emits `next` event', () => {
+ beforeEach(async () => {
+ findSetupInstructions().vm.$emit('next');
+ await nextTick();
+ });
+
+ it('renders sign in button', () => {
+ expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath);
+ });
+
+ it('hides setup instructions', () => {
+ expect(findSetupInstructions().exists()).toBe(false);
+ });
+ });
});
- describe('sign in button', () => {
+ describe('when on GitLab.com', () => {
+ beforeEach(() => {
+ retrieveBaseUrl.mockReturnValue(GITLAB_COM_BASE_PATH);
+ createComponent();
+ });
+
+ it('does not render setup instructions', () => {
+ expect(findSetupInstructions().exists()).toBe(false);
+ });
+
it('renders sign in button', () => {
- expect(findSignInOauthButton().exists()).toBe(true);
- expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath);
+ expect(findSignInOauthButton().props('gitlabBasePath')).toBe(GITLAB_COM_BASE_PATH);
});
describe('when button emits `sign-in` event', () => {
@@ -90,9 +126,5 @@ describe('SignInGitlabMultiversion', () => {
});
});
});
-
- it('renders correct subtitle', () => {
- expect(findSubtitle().text()).toBe(SignInGitlabMultiversion.i18n.signInSubtitle);
- });
});
});
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
new file mode 100644
index 00000000000..5496cf008c5
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js
@@ -0,0 +1,35 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton, GlLink } from '@gitlab/ui';
+
+import { OAUTH_SELF_MANAGED_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 findGlButton = () => wrapper.findComponent(GlButton);
+ const findGlLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = () => {
+ wrapper = shallowMount(SetupInstructions);
+ };
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders "Learn more" link to documentation', () => {
+ expect(findGlLink().attributes('href')).toBe(OAUTH_SELF_MANAGED_DOC_LINK);
+ });
+
+ describe('when button is clicked', () => {
+ it('emits "next" event', () => {
+ expect(wrapper.emitted('next')).toBeUndefined();
+ findGlButton().vm.$emit('click');
+
+ expect(wrapper.emitted('next')).toHaveLength(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
index a72528ae36b..748e151f31b 100644
--- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
+++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
@@ -87,7 +87,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
>
<div
aria-label="The GitLab user to which the Jira user Jane Doe will be mapped"
- class="dropdown b-dropdown gl-new-dropdown w-100 btn-group"
+ class="dropdown b-dropdown gl-dropdown w-100 btn-group"
>
<!---->
<button
@@ -101,7 +101,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
<span
- class="gl-new-dropdown-button-text"
+ class="gl-dropdown-button-text"
>
janedoe
</span>
@@ -123,14 +123,14 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
tabindex="-1"
>
<div
- class="gl-new-dropdown-inner"
+ class="gl-dropdown-inner"
>
<!---->
<!---->
<div
- class="gl-new-dropdown-contents"
+ class="gl-dropdown-contents"
>
<!---->
@@ -165,7 +165,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
</div>
<li
- class="gl-new-dropdown-text text-secondary"
+ class="gl-dropdown-text text-secondary"
role="presentation"
>
<p
@@ -218,7 +218,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
>
<div
aria-label="The GitLab user to which the Jira user Fred Chopin will be mapped"
- class="dropdown b-dropdown gl-new-dropdown w-100 btn-group"
+ class="dropdown b-dropdown gl-dropdown w-100 btn-group"
>
<!---->
<button
@@ -232,7 +232,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
<span
- class="gl-new-dropdown-button-text"
+ class="gl-dropdown-button-text"
>
mrgitlab
</span>
@@ -254,14 +254,14 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
tabindex="-1"
>
<div
- class="gl-new-dropdown-inner"
+ class="gl-dropdown-inner"
>
<!---->
<!---->
<div
- class="gl-new-dropdown-contents"
+ class="gl-dropdown-contents"
>
<!---->
@@ -296,7 +296,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
</div>
<li
- class="gl-new-dropdown-text text-secondary"
+ class="gl-dropdown-text text-secondary"
role="presentation"
>
<p
diff --git a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
index 98bdfc3fcbc..14613775791 100644
--- a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
+++ b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
@@ -1,6 +1,10 @@
import { GlFilteredSearch } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ OPERATORS_IS,
+ TOKEN_TITLE_STATUS,
+ TOKEN_TYPE_STATUS,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
import { mockFailedSearchToken } from '../../mock_data';
@@ -37,11 +41,11 @@ describe('Jobs filtered search', () => {
createComponent();
expect(findStatusToken()).toMatchObject({
- type: 'status',
+ type: TOKEN_TYPE_STATUS,
icon: 'status',
- title: 'Status',
+ title: TOKEN_TITLE_STATUS,
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
});
});
@@ -65,7 +69,7 @@ describe('Jobs filtered search', () => {
createComponent({ queryString: { statuses: value } });
expect(findFilteredSearch().props('value')).toEqual([
- { type: 'status', value: { data: value, operator: '=' } },
+ { type: TOKEN_TYPE_STATUS, value: { data: value, operator: '=' } },
]);
});
});
diff --git a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
index 92ce3925a90..fbe5f6a2e11 100644
--- a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
+++ b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
@@ -2,6 +2,10 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitl
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import JobStatusToken from '~/jobs/components/filtered_search/tokens/job_status_token.vue';
+import {
+ TOKEN_TITLE_STATUS,
+ TOKEN_TYPE_STATUS,
+} from '~/vue_shared/components/filtered_search_bar/constants';
describe('Job Status Token', () => {
let wrapper;
@@ -13,9 +17,9 @@ describe('Job Status Token', () => {
const defaultProps = {
config: {
- type: 'status',
+ type: TOKEN_TYPE_STATUS,
icon: 'status',
- title: 'Status',
+ title: TOKEN_TITLE_STATUS,
unique: true,
},
value: {
diff --git a/spec/frontend/jobs/components/job/empty_state_spec.js b/spec/frontend/jobs/components/job/empty_state_spec.js
index 299b607ad78..c6ab259bf46 100644
--- a/spec/frontend/jobs/components/job/empty_state_spec.js
+++ b/spec/frontend/jobs/components/job/empty_state_spec.js
@@ -1,5 +1,7 @@
-import { mount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import EmptyState from '~/jobs/components/job/empty_state.vue';
+import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
+import { mockFullPath, mockId } from './mock_data';
describe('Empty State', () => {
let wrapper;
@@ -7,26 +9,31 @@ describe('Empty State', () => {
const defaultProps = {
illustrationPath: 'illustrations/pending_job_empty.svg',
illustrationSizeClass: 'svg-430',
+ jobId: mockId,
title: 'This job has not started yet',
playable: false,
+ isRetryable: true,
};
const createWrapper = (props) => {
- wrapper = mount(EmptyState, {
+ wrapper = shallowMountExtended(EmptyState, {
propsData: {
...defaultProps,
...props,
},
+ provide: {
+ projectPath: mockFullPath,
+ },
});
};
const content = 'This job is in pending state and is waiting to be picked by a runner';
const findEmptyStateImage = () => wrapper.find('img');
- const findTitle = () => wrapper.find('[data-testid="job-empty-state-title"]');
- const findContent = () => wrapper.find('[data-testid="job-empty-state-content"]');
- const findAction = () => wrapper.find('[data-testid="job-empty-state-action"]');
- const findManualVarsForm = () => wrapper.find('[data-testid="manual-vars-form"]');
+ const findTitle = () => wrapper.findByTestId('job-empty-state-title');
+ const findContent = () => wrapper.findByTestId('job-empty-state-content');
+ const findAction = () => wrapper.findByTestId('job-empty-state-action');
+ const findManualVarsForm = () => wrapper.findComponent(ManualVariablesForm);
afterEach(() => {
if (wrapper?.destroy) {
diff --git a/spec/frontend/jobs/components/job/job_app_spec.js b/spec/frontend/jobs/components/job/job_app_spec.js
index 822528403cf..98f1979db1b 100644
--- a/spec/frontend/jobs/components/job/job_app_spec.js
+++ b/spec/frontend/jobs/components/job/job_app_spec.js
@@ -1,14 +1,15 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
-import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
-import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
+import { GlLoadingIcon } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import EmptyState from '~/jobs/components/job/empty_state.vue';
import EnvironmentsBlock from '~/jobs/components/job/environments_block.vue';
import ErasedBlock from '~/jobs/components/job/erased_block.vue';
import JobApp from '~/jobs/components/job/job_app.vue';
+import JobLog from '~/jobs/components/log/log.vue';
+import JobLogTopBar from '~/jobs/components/job/job_log_controllers.vue';
import Sidebar from '~/jobs/components/job/sidebar/sidebar.vue';
import StuckBlock from '~/jobs/components/job/stuck_block.vue';
import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue';
@@ -40,7 +41,10 @@ describe('Job App', () => {
};
const createComponent = () => {
- wrapper = mount(JobApp, { propsData: { ...props }, store });
+ wrapper = shallowMountExtended(JobApp, {
+ propsData: { ...props },
+ store,
+ });
};
const setupAndMount = async ({ jobData = {}, jobLogData = {} } = {}) => {
@@ -59,22 +63,16 @@ describe('Job App', () => {
const findLoadingComponent = () => wrapper.findComponent(GlLoadingIcon);
const findSidebar = () => wrapper.findComponent(Sidebar);
- const findJobContent = () => wrapper.find('[data-testid="job-content"');
const findStuckBlockComponent = () => wrapper.findComponent(StuckBlock);
- const findStuckBlockWithTags = () => wrapper.find('[data-testid="job-stuck-with-tags"');
- const findStuckBlockNoActiveRunners = () =>
- wrapper.find('[data-testid="job-stuck-no-active-runners"');
const findFailedJobComponent = () => wrapper.findComponent(UnmetPrerequisitesBlock);
const findEnvironmentsBlockComponent = () => wrapper.findComponent(EnvironmentsBlock);
const findErasedBlock = () => wrapper.findComponent(ErasedBlock);
- const findArchivedJob = () => wrapper.find('[data-testid="archived-job"]');
const findEmptyState = () => wrapper.findComponent(EmptyState);
- const findJobNewIssueLink = () => wrapper.find('[data-testid="job-new-issue"]');
- const findJobEmptyStateTitle = () => wrapper.find('[data-testid="job-empty-state-title"]');
- const findJobLogScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]');
- const findJobLogScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]');
- const findJobLogController = () => wrapper.find('[data-testid="job-raw-link-controller"]');
- const findJobLogEraseLink = () => wrapper.find('[data-testid="job-log-erase-link"]');
+ const findJobLog = () => wrapper.findComponent(JobLog);
+ const findJobLogTopBar = () => wrapper.findComponent(JobLogTopBar);
+
+ const findJobContent = () => wrapper.findByTestId('job-content');
+ const findArchivedJob = () => wrapper.findByTestId('archived-job');
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -116,36 +114,6 @@ describe('Job App', () => {
expect(wrapper.vm.shouldRenderCalloutMessage).toBe(true);
}));
});
-
- describe('triggered job', () => {
- beforeEach(() => {
- const aYearAgo = new Date();
- aYearAgo.setFullYear(aYearAgo.getFullYear() - 1);
-
- return setupAndMount({
- jobData: { started: aYearAgo.toISOString(), started_at: aYearAgo.toISOString() },
- });
- });
-
- it('should render provided job information', () => {
- expect(wrapper.find('.header-main-content').text().replace(/\s+/g, ' ').trim()).toContain(
- 'passed Job test triggered 1 year ago by Root',
- );
- });
-
- it('should render new issue link', () => {
- expect(findJobNewIssueLink().attributes('href')).toEqual(job.new_issue_path);
- });
- });
-
- describe('created job', () => {
- it('should render created key', () =>
- setupAndMount().then(() => {
- expect(
- wrapper.find('.header-main-content').text().replace(/\s+/g, ' ').trim(),
- ).toContain('passed Job test created 3 weeks ago by Root');
- }));
- });
});
describe('stuck block', () => {
@@ -169,57 +137,10 @@ describe('Job App', () => {
},
}).then(() => {
expect(findStuckBlockComponent().exists()).toBe(true);
- expect(findStuckBlockNoActiveRunners().exists()).toBe(true);
- }));
- });
-
- describe('when available runners can not run specified tag', () => {
- it('renders tags in stuck block when there are no runners', () =>
- setupAndMount({
- jobData: {
- status: {
- group: 'pending',
- icon: 'status_pending',
- label: 'pending',
- text: 'pending',
- details_path: 'path',
- },
- stuck: true,
- runners: {
- available: false,
- online: false,
- },
- },
- }).then(() => {
- expect(findStuckBlockComponent().text()).toContain(job.tags[0]);
- expect(findStuckBlockWithTags().exists()).toBe(true);
- }));
- });
-
- describe('when runners are offline and build has tags', () => {
- it('renders message about job being stuck because of no runners with the specified tags', () =>
- setupAndMount({
- jobData: {
- status: {
- group: 'pending',
- icon: 'status_pending',
- label: 'pending',
- text: 'pending',
- details_path: 'path',
- },
- stuck: true,
- runners: {
- available: true,
- online: true,
- },
- },
- }).then(() => {
- expect(findStuckBlockComponent().text()).toContain(job.tags[0]);
- expect(findStuckBlockWithTags().exists()).toBe(true);
}));
});
- it('does not renders stuck block when there are no runners', () =>
+ it('does not render stuck block when there are runners', () =>
setupAndMount({
jobData: {
runners: { available: true },
@@ -351,45 +272,13 @@ describe('Job App', () => {
setupAndMount({ jobData: { has_trace: true } }).then(() => {
expect(findEmptyState().exists()).toBe(false);
}));
-
- it('displays remaining time for a delayed job', () => {
- const oneHourInMilliseconds = 3600000;
- jest
- .spyOn(Date, 'now')
- .mockImplementation(
- () => new Date(delayedJobFixture.scheduled_at).getTime() - oneHourInMilliseconds,
- );
- return setupAndMount({ jobData: delayedJobFixture }).then(() => {
- expect(findEmptyState().exists()).toBe(true);
-
- const title = findJobEmptyStateTitle().text();
-
- expect(title).toEqual('This is a delayed job to run in 01:00:00');
- });
- });
});
describe('sidebar', () => {
- it('has no blank blocks', async () => {
- await setupAndMount({
- jobData: {
- duration: null,
- finished_at: null,
- erased_at: null,
- queued: null,
- runner: null,
- coverage: null,
- tags: [],
- cancel_path: null,
- },
- });
+ it('renders sidebar', async () => {
+ await setupAndMount();
- const blocks = wrapper.findAll('.blocks-container > *').wrappers;
- expect(blocks.length).toBeGreaterThan(0);
-
- blocks.forEach((block) => {
- expect(block.text().trim()).not.toBe('');
- });
+ expect(findSidebar().exists()).toBe(true);
});
});
});
@@ -410,31 +299,15 @@ describe('Job App', () => {
});
});
- describe('job log controls', () => {
- beforeEach(() =>
- setupAndMount({
- jobLogData: {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 50,
- total: 100,
- complete: true,
- },
- }),
- );
-
- it('should render scroll buttons', () => {
- expect(findJobLogScrollTop().exists()).toBe(true);
- expect(findJobLogScrollBottom().exists()).toBe(true);
- });
+ describe('job log', () => {
+ beforeEach(() => setupAndMount());
- it('should render link to raw ouput', () => {
- expect(findJobLogController().exists()).toBe(true);
+ it('should render job log header', () => {
+ expect(findJobLogTopBar().exists()).toBe(true);
});
- it('should render link to erase job', () => {
- expect(findJobLogEraseLink().exists()).toBe(true);
+ it('should render job log', () => {
+ expect(findJobLog().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
index 18d5f35bde4..91821a38a78 100644
--- a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
+++ b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
@@ -16,6 +16,7 @@ describe('Job Sidebar Retry Button', () => {
wrapper = shallowMountExtended(JobsSidebarRetryButton, {
propsData: {
href: job.retry_path,
+ isManualJob: false,
modalId: 'modal-id',
...props,
},
diff --git a/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js b/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js
deleted file mode 100644
index 184562b2968..00000000000
--- a/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js
+++ /dev/null
@@ -1,156 +0,0 @@
-import { GlSprintf, GlLink } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import LegacyManualVariablesForm from '~/jobs/components/job/legacy_manual_variables_form.vue';
-
-Vue.use(Vuex);
-
-describe('Manual Variables Form', () => {
- let wrapper;
- let store;
-
- const requiredProps = {
- action: {
- path: '/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- };
-
- const createComponent = (props = {}) => {
- store = new Vuex.Store({
- actions: {
- triggerManualJob: jest.fn(),
- },
- });
-
- wrapper = extendedWrapper(
- mount(LegacyManualVariablesForm, {
- propsData: { ...requiredProps, ...props },
- store,
- stubs: {
- GlSprintf,
- },
- }),
- );
- };
-
- const findHelpText = () => wrapper.findComponent(GlSprintf);
- const findHelpLink = () => wrapper.findComponent(GlLink);
-
- const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn');
- const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
- const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn');
- const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder');
- const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key');
- const findAllCiVariableKeys = () => wrapper.findAllByTestId('ci-variable-key');
- const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value');
- const findAllVariables = () => wrapper.findAllByTestId('ci-variable-row');
-
- const setCiVariableKey = () => {
- findCiVariableKey().setValue('new key');
- findCiVariableKey().vm.$emit('change');
- nextTick();
- };
-
- const setCiVariableKeyByPosition = (position, value) => {
- findAllCiVariableKeys().at(position).setValue(value);
- findAllCiVariableKeys().at(position).vm.$emit('change');
- nextTick();
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('creates a new variable when user enters a new key value', async () => {
- expect(findAllVariables()).toHaveLength(1);
-
- await setCiVariableKey();
-
- expect(findAllVariables()).toHaveLength(2);
- });
-
- it('does not create extra empty variables', async () => {
- expect(findAllVariables()).toHaveLength(1);
-
- await setCiVariableKey();
-
- expect(findAllVariables()).toHaveLength(2);
-
- await setCiVariableKey();
-
- expect(findAllVariables()).toHaveLength(2);
- });
-
- it('removes the correct variable row', async () => {
- const variableKeyNameOne = 'key-one';
- const variableKeyNameThree = 'key-three';
-
- await setCiVariableKeyByPosition(0, variableKeyNameOne);
-
- await setCiVariableKeyByPosition(1, 'key-two');
-
- await setCiVariableKeyByPosition(2, variableKeyNameThree);
-
- expect(findAllVariables()).toHaveLength(4);
-
- await findAllDeleteVarBtns().at(1).trigger('click');
-
- expect(findAllVariables()).toHaveLength(3);
-
- expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
- expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
- expect(findAllCiVariableKeys().at(2).element.value).toBe('');
- });
-
- it('trigger button is disabled after trigger action', async () => {
- expect(findTriggerBtn().props('disabled')).toBe(false);
-
- await findTriggerBtn().trigger('click');
-
- expect(findTriggerBtn().props('disabled')).toBe(true);
- });
-
- it('delete variable button should only show when there is more than one variable', async () => {
- expect(findDeleteVarBtn().exists()).toBe(false);
-
- await setCiVariableKey();
-
- expect(findDeleteVarBtn().exists()).toBe(true);
- });
-
- it('delete variable button placeholder should only exist when a user cannot remove', async () => {
- expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
- });
-
- it('renders help text with provided link', () => {
- expect(findHelpText().exists()).toBe(true);
- expect(findHelpLink().attributes('href')).toBe(
- '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
- );
- });
-
- it('passes variables in correct format', async () => {
- jest.spyOn(store, 'dispatch');
-
- await setCiVariableKey();
-
- await findCiVariableValue().setValue('new value');
-
- await findTriggerBtn().trigger('click');
-
- expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [
- {
- key: 'new key',
- secret_value: 'new value',
- },
- ]);
- });
-});
diff --git a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
deleted file mode 100644
index 95eb10118ee..00000000000
--- a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue';
-import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue';
-import createStore from '~/jobs/store';
-import job, { failedJobStatus } from '../../mock_data';
-
-describe('Legacy Sidebar Header', () => {
- let store;
- let wrapper;
-
- const findCancelButton = () => wrapper.findByTestId('cancel-button');
- const findRetryButton = () => wrapper.findComponent(JobRetryButton);
- const findEraseLink = () => wrapper.findByTestId('job-log-erase-link');
-
- const createWrapper = (props) => {
- store = createStore();
-
- wrapper = extendedWrapper(
- shallowMount(LegacySidebarHeader, {
- propsData: {
- job,
- ...props,
- },
- store,
- }),
- );
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when job log is erasable', () => {
- const path = '/root/ci-project/-/jobs/1447/erase';
-
- beforeEach(() => {
- createWrapper({
- erasePath: path,
- });
- });
-
- it('renders erase job link', () => {
- expect(findEraseLink().exists()).toBe(true);
- });
-
- it('erase job link has correct path', () => {
- expect(findEraseLink().attributes('href')).toBe(path);
- });
- });
-
- describe('when job log is not erasable', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('does not render erase button', () => {
- expect(findEraseLink().exists()).toBe(false);
- });
- });
-
- describe('when the job is retryable', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('should render the retry button', () => {
- expect(findRetryButton().props('href')).toBe(job.retry_path);
- });
-
- it('should have a different label when the job status is passed', () => {
- expect(findRetryButton().attributes('title')).toBe(
- LegacySidebarHeader.i18n.runAgainJobButtonLabel,
- );
- });
- });
-
- describe('when there is no retry path', () => {
- it('should not render a retry button', async () => {
- const copy = { ...job, retry_path: null };
- createWrapper({ job: copy });
-
- expect(findRetryButton().exists()).toBe(false);
- });
- });
-
- describe('when the job is cancelable', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('should render link to cancel job', () => {
- expect(findCancelButton().props('icon')).toBe('cancel');
- expect(findCancelButton().attributes('href')).toBe(job.cancel_path);
- });
- });
-
- describe('when the job is failed', () => {
- describe('retry button', () => {
- it('should have a different label when the job status is failed', () => {
- createWrapper({ job: { ...job, status: failedJobStatus } });
-
- expect(findRetryButton().attributes('title')).toBe(
- LegacySidebarHeader.i18n.retryJobButtonLabel,
- );
- });
- });
- });
-});
diff --git a/spec/frontend/jobs/components/job/manual_variables_form_spec.js b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
index 5806f9f75f9..45a1e9dca76 100644
--- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
@@ -1,46 +1,71 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { GRAPHQL_ID_TYPES } from '~/jobs/constants';
+import waitForPromises from 'helpers/wait_for_promises';
import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
-
-Vue.use(Vuex);
+import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql';
+import retryJobMutation from '~/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql';
+import {
+ mockFullPath,
+ mockId,
+ mockJobResponse,
+ mockJobWithVariablesResponse,
+ mockJobMutationData,
+} from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const defaultProvide = {
+ projectPath: mockFullPath,
+};
describe('Manual Variables Form', () => {
let wrapper;
- let store;
-
- const requiredProps = {
- action: {
- path: '/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
+ let mockApollo;
+ let getJobQueryResponse;
+
+ const createComponent = ({ options = {}, props = {} } = {}) => {
+ wrapper = mountExtended(ManualVariablesForm, {
+ propsData: {
+ ...props,
+ jobId: mockId,
+ isRetryable: true,
+ },
+ provide: {
+ ...defaultProvide,
+ },
+ ...options,
+ });
};
- const createComponent = (props = {}) => {
- store = new Vuex.Store({
- actions: {
- triggerManualJob: jest.fn(),
- },
+ const createComponentWithApollo = async ({ props = {} } = {}) => {
+ const requestHandlers = [[getJobQuery, getJobQueryResponse]];
+
+ mockApollo = createMockApollo(requestHandlers);
+
+ const options = {
+ localVue,
+ apolloProvider: mockApollo,
+ };
+
+ createComponent({
+ props,
+ options,
});
- wrapper = extendedWrapper(
- mount(ManualVariablesForm, {
- propsData: { ...requiredProps, ...props },
- store,
- stubs: {
- GlSprintf,
- },
- }),
- );
+ return waitForPromises();
};
const findHelpText = () => wrapper.findComponent(GlSprintf);
const findHelpLink = () => wrapper.findComponent(GlLink);
-
- const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn');
+ const findCancelBtn = () => wrapper.findByTestId('cancel-btn');
+ const findRerunBtn = () => wrapper.findByTestId('run-manual-job-btn');
const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn');
const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder');
@@ -62,95 +87,134 @@ describe('Manual Variables Form', () => {
};
beforeEach(() => {
- createComponent();
+ getJobQueryResponse = jest.fn();
});
afterEach(() => {
wrapper.destroy();
});
- it('creates a new variable when user enters a new key value', async () => {
- expect(findAllVariables()).toHaveLength(1);
+ describe('when page renders', () => {
+ beforeEach(async () => {
+ getJobQueryResponse.mockResolvedValue(mockJobResponse);
+ await createComponentWithApollo();
+ });
+
+ it('renders help text with provided link', () => {
+ expect(findHelpText().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(
+ '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
+ );
+ });
+
+ it('renders buttons', () => {
+ expect(findCancelBtn().exists()).toBe(true);
+ expect(findRerunBtn().exists()).toBe(true);
+ });
+ });
+
+ describe('when job has variables', () => {
+ beforeEach(async () => {
+ getJobQueryResponse.mockResolvedValue(mockJobWithVariablesResponse);
+ await createComponentWithApollo();
+ });
- await setCiVariableKey();
+ it('sets manual job variables', () => {
+ const queryKey = mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].key;
+ const queryValue =
+ mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].value;
- expect(findAllVariables()).toHaveLength(2);
+ expect(findCiVariableKey().element.value).toBe(queryKey);
+ expect(findCiVariableValue().element.value).toBe(queryValue);
+ });
});
- it('does not create extra empty variables', async () => {
- expect(findAllVariables()).toHaveLength(1);
+ describe('when mutation fires', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo();
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobMutationData);
+ });
- await setCiVariableKey();
+ it('passes variables in correct format', async () => {
+ await setCiVariableKey();
- expect(findAllVariables()).toHaveLength(2);
+ await findCiVariableValue().setValue('new value');
- await setCiVariableKey();
+ await findRerunBtn().vm.$emit('click');
- expect(findAllVariables()).toHaveLength(2);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: retryJobMutation,
+ variables: {
+ id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, mockId),
+ variables: [
+ {
+ key: 'new key',
+ value: 'new value',
+ },
+ ],
+ },
+ });
+ });
});
- it('removes the correct variable row', async () => {
- const variableKeyNameOne = 'key-one';
- const variableKeyNameThree = 'key-three';
+ describe('updating variables in UI', () => {
+ beforeEach(async () => {
+ getJobQueryResponse.mockResolvedValue(mockJobResponse);
+ await createComponentWithApollo();
+ });
- await setCiVariableKeyByPosition(0, variableKeyNameOne);
+ it('creates a new variable when user enters a new key value', async () => {
+ expect(findAllVariables()).toHaveLength(1);
- await setCiVariableKeyByPosition(1, 'key-two');
+ await setCiVariableKey();
- await setCiVariableKeyByPosition(2, variableKeyNameThree);
+ expect(findAllVariables()).toHaveLength(2);
+ });
- expect(findAllVariables()).toHaveLength(4);
+ it('does not create extra empty variables', async () => {
+ expect(findAllVariables()).toHaveLength(1);
- await findAllDeleteVarBtns().at(1).trigger('click');
+ await setCiVariableKey();
- expect(findAllVariables()).toHaveLength(3);
+ expect(findAllVariables()).toHaveLength(2);
- expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
- expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
- expect(findAllCiVariableKeys().at(2).element.value).toBe('');
- });
+ await setCiVariableKey();
- it('trigger button is disabled after trigger action', async () => {
- expect(findTriggerBtn().props('disabled')).toBe(false);
+ expect(findAllVariables()).toHaveLength(2);
+ });
- await findTriggerBtn().trigger('click');
+ it('removes the correct variable row', async () => {
+ const variableKeyNameOne = 'key-one';
+ const variableKeyNameThree = 'key-three';
- expect(findTriggerBtn().props('disabled')).toBe(true);
- });
+ await setCiVariableKeyByPosition(0, variableKeyNameOne);
- it('delete variable button should only show when there is more than one variable', async () => {
- expect(findDeleteVarBtn().exists()).toBe(false);
+ await setCiVariableKeyByPosition(1, 'key-two');
- await setCiVariableKey();
+ await setCiVariableKeyByPosition(2, variableKeyNameThree);
- expect(findDeleteVarBtn().exists()).toBe(true);
- });
+ expect(findAllVariables()).toHaveLength(4);
- it('delete variable button placeholder should only exist when a user cannot remove', async () => {
- expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
- });
+ await findAllDeleteVarBtns().at(1).trigger('click');
- it('renders help text with provided link', () => {
- expect(findHelpText().exists()).toBe(true);
- expect(findHelpLink().attributes('href')).toBe(
- '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
- );
- });
+ expect(findAllVariables()).toHaveLength(3);
- it('passes variables in correct format', async () => {
- jest.spyOn(store, 'dispatch');
+ expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
+ expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
+ expect(findAllCiVariableKeys().at(2).element.value).toBe('');
+ });
- await setCiVariableKey();
+ it('delete variable button should only show when there is more than one variable', async () => {
+ expect(findDeleteVarBtn().exists()).toBe(false);
- await findCiVariableValue().setValue('new value');
+ await setCiVariableKey();
- await findTriggerBtn().trigger('click');
+ expect(findDeleteVarBtn().exists()).toBe(true);
+ });
- expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [
- {
- key: 'new key',
- secret_value: 'new value',
- },
- ]);
+ it('delete variable button placeholder should only exist when a user cannot remove', async () => {
+ expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/jobs/components/job/mock_data.js b/spec/frontend/jobs/components/job/mock_data.js
new file mode 100644
index 00000000000..9596e859475
--- /dev/null
+++ b/spec/frontend/jobs/components/job/mock_data.js
@@ -0,0 +1,76 @@
+export const mockFullPath = 'Commit451/lab-coat';
+export const mockId = 401;
+
+export const mockJobResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/4',
+ job: {
+ id: 'gid://gitlab/Ci::Build/401',
+ manualJob: true,
+ manualVariables: {
+ nodes: [],
+ __typename: 'CiManualVariableConnection',
+ },
+ name: 'manual_job',
+ retryable: true,
+ status: 'SUCCESS',
+ __typename: 'CiJob',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockJobWithVariablesResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/4',
+ job: {
+ id: 'gid://gitlab/Ci::Build/401',
+ manualJob: true,
+ manualVariables: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::JobVariable/150',
+ key: 'new key',
+ value: 'new value',
+ __typename: 'CiManualVariable',
+ },
+ ],
+ __typename: 'CiManualVariableConnection',
+ },
+ name: 'manual_job',
+ retryable: true,
+ status: 'SUCCESS',
+ __typename: 'CiJob',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockJobMutationData = {
+ data: {
+ jobRetry: {
+ job: {
+ id: 'gid://gitlab/Ci::Build/401',
+ manualVariables: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::JobVariable/151',
+ key: 'new key',
+ value: 'new value',
+ __typename: 'CiManualVariable',
+ },
+ ],
+ __typename: 'CiManualVariableConnection',
+ },
+ webPath: '/Commit451/lab-coat/-/jobs/401',
+ __typename: 'CiJob',
+ },
+ errors: [],
+ __typename: 'JobRetryPayload',
+ },
+ },
+};
diff --git a/spec/frontend/jobs/components/job/sidebar_header_spec.js b/spec/frontend/jobs/components/job/sidebar_header_spec.js
index cb32ca9d3dc..da97945f9bf 100644
--- a/spec/frontend/jobs/components/job/sidebar_header_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_header_spec.js
@@ -1,91 +1,87 @@
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+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 SidebarHeader from '~/jobs/components/job/sidebar/sidebar_header.vue';
import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue';
-import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue';
-import createStore from '~/jobs/store';
-import job from '../../mock_data';
+import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql';
+import { mockFullPath, mockId, mockJobResponse } from './mock_data';
-describe('Legacy Sidebar Header', () => {
- let store;
+Vue.use(VueApollo);
+
+const defaultProvide = {
+ projectPath: mockFullPath,
+};
+
+describe('Sidebar Header', () => {
let wrapper;
- const findCancelButton = () => wrapper.findByTestId('cancel-button');
- const findRetryButton = () => wrapper.findComponent(JobRetryButton);
- const findEraseLink = () => wrapper.findByTestId('job-log-erase-link');
-
- const createWrapper = (props) => {
- store = createStore();
-
- wrapper = extendedWrapper(
- shallowMount(LegacySidebarHeader, {
- propsData: {
- job,
- ...props,
- },
- store,
- }),
- );
+ const createComponent = ({ options = {}, props = {}, restJob = {} } = {}) => {
+ wrapper = shallowMountExtended(SidebarHeader, {
+ propsData: {
+ ...props,
+ jobId: mockId,
+ restJob,
+ },
+ provide: {
+ ...defaultProvide,
+ },
+ ...options,
+ });
};
- afterEach(() => {
- wrapper.destroy();
- });
+ const createComponentWithApollo = async ({ props = {}, restJob = {} } = {}) => {
+ const getJobQueryResponse = jest.fn().mockResolvedValue(mockJobResponse);
- describe('when job log is erasable', () => {
- const path = '/root/ci-project/-/jobs/1447/erase';
+ const requestHandlers = [[getJobQuery, getJobQueryResponse]];
- beforeEach(() => {
- createWrapper({
- erasePath: path,
- });
- });
+ const apolloProvider = createMockApollo(requestHandlers);
- it('renders erase job link', () => {
- expect(findEraseLink().exists()).toBe(true);
- });
+ const options = {
+ apolloProvider,
+ };
- it('erase job link has correct path', () => {
- expect(findEraseLink().attributes('href')).toBe(path);
+ createComponent({
+ props,
+ restJob,
+ options,
});
- });
- describe('when job log is not erasable', () => {
- beforeEach(() => {
- createWrapper();
- });
+ return waitForPromises();
+ };
- it('does not render erase button', () => {
- expect(findEraseLink().exists()).toBe(false);
- });
- });
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+ const findEraseButton = () => wrapper.findByTestId('job-log-erase-link');
+ const findJobName = () => wrapper.findByTestId('job-name');
+ const findRetryButton = () => wrapper.findComponent(JobRetryButton);
- describe('when the job is retryable', () => {
- beforeEach(() => {
- createWrapper();
+ describe('when rendering contents', () => {
+ it('renders the correct job name', async () => {
+ await createComponentWithApollo();
+ expect(findJobName().text()).toBe(mockJobResponse.data.project.job.name);
});
- it('should render the retry button', () => {
- expect(findRetryButton().props('href')).toBe(job.retry_path);
+ it('does not render buttons with no paths', async () => {
+ await createComponentWithApollo();
+ expect(findCancelButton().exists()).toBe(false);
+ expect(findEraseButton().exists()).toBe(false);
+ expect(findRetryButton().exists()).toBe(false);
});
- });
-
- describe('when there is no retry path', () => {
- it('should not render a retry button', async () => {
- const copy = { ...job, retry_path: null };
- createWrapper({ job: copy });
- expect(findRetryButton().exists()).toBe(false);
+ it('renders a retry button with a path', async () => {
+ await createComponentWithApollo({ restJob: { retry_path: 'retry/path' } });
+ expect(findRetryButton().exists()).toBe(true);
});
- });
- describe('when the job is cancelable', () => {
- beforeEach(() => {
- createWrapper();
+ it('renders a cancel button with a path', async () => {
+ await createComponentWithApollo({ restJob: { cancel_path: 'cancel/path' } });
+ expect(findCancelButton().exists()).toBe(true);
});
- it('should render link to cancel job', () => {
- expect(findCancelButton().props('icon')).toBe('cancel');
- expect(findCancelButton().attributes('href')).toBe(job.cancel_path);
+ it('renders an erase button with a path', async () => {
+ await createComponentWithApollo({ restJob: { erase_path: 'erase/path' } });
+ expect(findEraseButton().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index a7fe6d5a626..9abd610c26d 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -3,6 +3,7 @@ import mockJobsPaginated from 'test_fixtures/graphql/jobs/get_jobs.query.graphql
import mockJobs from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.json';
import mockJobsAsGuest from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.as_guest.json';
import { TEST_HOST } from 'spec/test_constants';
+import { TOKEN_TYPE_STATUS } from '~/vue_shared/components/filtered_search_bar/constants';
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
@@ -1365,7 +1366,10 @@ export const CIJobConnectionExistingCache = {
statuses: 'PENDING',
};
-export const mockFailedSearchToken = { type: 'status', value: { data: 'FAILED', operator: '=' } };
+export const mockFailedSearchToken = {
+ type: TOKEN_TYPE_STATUS,
+ value: { data: 'FAILED', operator: '=' },
+};
export const retryMutationResponse = {
data: {
diff --git a/spec/frontend/language_switcher/components/app_spec.js b/spec/frontend/language_switcher/components/app_spec.js
new file mode 100644
index 00000000000..6a1b94cd813
--- /dev/null
+++ b/spec/frontend/language_switcher/components/app_spec.js
@@ -0,0 +1,62 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import LanguageSwitcherApp from '~/language_switcher/components/app.vue';
+import { PREFERRED_LANGUAGE_COOKIE_KEY } from '~/language_switcher/constants';
+import * as utils from '~/lib/utils/common_utils';
+import { locales, ES, EN } from '../mock_data';
+
+jest.mock('~/lib/utils/common_utils');
+
+describe('<LanguageSwitcher />', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mountExtended(LanguageSwitcherApp, {
+ provide: {
+ locales,
+ preferredLocale: EN,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const getPreferredLanguage = () => wrapper.find('.gl-dropdown-button-text').text();
+ const findLanguageDropdownItem = (code) => wrapper.findByTestId(`language_switcher_lang_${code}`);
+
+ it('preferred language', () => {
+ expect(getPreferredLanguage()).toBe(EN.text);
+
+ createComponent({
+ preferredLocale: ES,
+ });
+
+ expect(getPreferredLanguage()).toBe(ES.text);
+ });
+
+ it('switches language', async () => {
+ // because window.location is **READ ONLY** we cannot simply use
+ // jest.spyOn to mock it.
+ const originalLocation = window.location;
+ delete window.location;
+ window.location = {};
+ window.location.reload = jest.fn();
+ const reloadSpy = window.location.reload;
+ expect(reloadSpy).not.toHaveBeenCalled();
+ expect(utils.setCookie).not.toHaveBeenCalled();
+
+ const es = findLanguageDropdownItem(ES.value);
+
+ await es.trigger('click');
+
+ expect(reloadSpy).toHaveBeenCalled();
+ expect(utils.setCookie).toHaveBeenCalledWith(PREFERRED_LANGUAGE_COOKIE_KEY, ES.value);
+ window.location = originalLocation;
+ });
+});
diff --git a/spec/frontend/language_switcher/mock_data.js b/spec/frontend/language_switcher/mock_data.js
new file mode 100644
index 00000000000..548bddf0173
--- /dev/null
+++ b/spec/frontend/language_switcher/mock_data.js
@@ -0,0 +1,26 @@
+export const EN = {
+ value: 'en',
+ text: 'English',
+};
+
+export const ZH_CN = {
+ value: 'zh_CN',
+ text: '简体中文',
+};
+
+export const ES = {
+ value: 'es',
+ text: 'Espanol',
+};
+
+export const ZH_HK = {
+ value: 'zh_HK',
+ text: '繁体中文(香港)',
+};
+
+export const ZH_TW = {
+ value: 'zh_TW',
+ text: '繁体中文(台湾)',
+};
+
+export const locales = [EN, ZH_CN, ES, ZH_HK, ZH_TW];
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js
index 412408ce377..f767a673553 100644
--- a/spec/frontend/lib/dompurify_spec.js
+++ b/spec/frontend/lib/dompurify_spec.js
@@ -94,6 +94,11 @@ describe('~/lib/dompurify', () => {
expect(sanitize('<link rel="stylesheet" href="styles.css">')).toBe('');
});
+ it("doesn't allow form tags", () => {
+ expect(sanitize('<form>')).toBe('');
+ expect(sanitize('<form method="post" action="path"></form>')).toBe('');
+ });
+
describe.each`
type | gon
${'root'} | ${rootGon}
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 947c38c8ae8..08ba78cddff 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1,4 +1,5 @@
import * as commonUtils from '~/lib/utils/common_utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
describe('common_utils', () => {
describe('getPagePath', () => {
@@ -1069,4 +1070,35 @@ describe('common_utils', () => {
expect(result).toEqual([{ hello: '' }, { helloWorld: '' }]);
});
});
+
+ describe('useNewFonts', () => {
+ let beforeGon;
+ const beforeLocation = window.location.href;
+
+ beforeEach(() => {
+ window.gon = window.gon || {};
+ beforeGon = { ...window.gon };
+ });
+
+ describe.each`
+ featureFlag | queryParameter | fontEnabled
+ ${false} | ${false} | ${false}
+ ${true} | ${false} | ${true}
+ ${false} | ${true} | ${true}
+ `('new font', ({ featureFlag, queryParameter, fontEnabled }) => {
+ it(`will ${fontEnabled ? '' : 'NOT '}be applied when feature flag is ${
+ featureFlag ? '' : 'NOT '
+ }set and query parameter is ${queryParameter ? '' : 'NOT '}present`, () => {
+ const search = queryParameter ? `?new_fonts` : '';
+ setWindowLocation(search);
+ window.gon = { features: { newFonts: featureFlag } };
+ expect(commonUtils.useNewFonts()).toBe(fontEnabled);
+ });
+ });
+
+ afterEach(() => {
+ window.gon = beforeGon;
+ setWindowLocation(beforeLocation);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/create_and_submit_form_spec.js b/spec/frontend/lib/utils/create_and_submit_form_spec.js
new file mode 100644
index 00000000000..9f2472c60f7
--- /dev/null
+++ b/spec/frontend/lib/utils/create_and_submit_form_spec.js
@@ -0,0 +1,73 @@
+import csrf from '~/lib/utils/csrf';
+import { TEST_HOST } from 'helpers/test_constants';
+import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
+import { joinPaths } from '~/lib/utils/url_utility';
+
+const TEST_URL = '/foo/bar/lorem';
+const TEST_DATA = {
+ 'test_thing[0]': 'Lorem Ipsum',
+ 'test_thing[1]': 'Dolar Sit',
+ x: 123,
+};
+const TEST_CSRF = 'testcsrf00==';
+
+describe('~/lib/utils/create_and_submit_form', () => {
+ let submitSpy;
+
+ const findForm = () => document.querySelector('form');
+ const findInputsModel = () =>
+ Array.from(findForm().querySelectorAll('input')).map((inputEl) => ({
+ type: inputEl.type,
+ name: inputEl.name,
+ value: inputEl.value,
+ }));
+
+ beforeEach(() => {
+ submitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit');
+ document.head.innerHTML = `<meta name="csrf-token" content="${TEST_CSRF}">`;
+ csrf.init();
+ });
+
+ afterEach(() => {
+ document.head.innerHTML = '';
+ document.body.innerHTML = '';
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createAndSubmitForm({
+ url: TEST_URL,
+ data: TEST_DATA,
+ });
+ });
+
+ it('creates form', () => {
+ const form = findForm();
+
+ expect(form.action).toBe(joinPaths(TEST_HOST, TEST_URL));
+ expect(form.method).toBe('post');
+ expect(form.style).toMatchObject({
+ display: 'none',
+ });
+ });
+
+ it('creates inputs', () => {
+ expect(findInputsModel()).toEqual([
+ ...Object.keys(TEST_DATA).map((key) => ({
+ type: 'hidden',
+ name: key,
+ value: String(TEST_DATA[key]),
+ })),
+ {
+ type: 'hidden',
+ name: 'authenticity_token',
+ value: TEST_CSRF,
+ },
+ ]);
+ });
+
+ it('submits form', () => {
+ expect(submitSpy).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/dom_utils_spec.js b/spec/frontend/lib/utils/dom_utils_spec.js
index d6bac935970..172f8972653 100644
--- a/spec/frontend/lib/utils/dom_utils_spec.js
+++ b/spec/frontend/lib/utils/dom_utils_spec.js
@@ -10,6 +10,7 @@ import {
getParents,
getParentByTagName,
setAttributes,
+ replaceCommentsWith,
} from '~/lib/utils/dom_utils';
const TEST_MARGIN = 5;
@@ -263,4 +264,21 @@ describe('DOM Utils', () => {
expect(getContentWrapperHeight('.does-not-exist')).toBe('');
});
});
+
+ describe('replaceCommentsWith', () => {
+ let div;
+ beforeEach(() => {
+ div = document.createElement('div');
+ });
+
+ it('replaces the comments in a DOM node with an element', () => {
+ div.innerHTML = '<h1> hi there <!-- some comment --> <p> <!-- another comment -->';
+
+ replaceCommentsWith(div, 'comment');
+
+ expect(div.innerHTML).toBe(
+ '<h1> hi there <comment> some comment </comment> <p> <comment> another comment </comment></p></h1>',
+ );
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/poll_until_complete_spec.js b/spec/frontend/lib/utils/poll_until_complete_spec.js
index 7509f954a84..3ce17ecfc8c 100644
--- a/spec/frontend/lib/utils/poll_until_complete_spec.js
+++ b/spec/frontend/lib/utils/poll_until_complete_spec.js
@@ -1,7 +1,7 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import pollUntilComplete from '~/lib/utils/poll_until_complete';
const endpoint = `${TEST_HOST}/foo`;
@@ -37,7 +37,7 @@ describe('pollUntilComplete', () => {
beforeEach(() => {
mock
.onGet(endpoint)
- .replyOnce(httpStatusCodes.NO_CONTENT, undefined, pollIntervalHeader)
+ .replyOnce(HTTP_STATUS_NO_CONTENT, undefined, pollIntervalHeader)
.onGet(endpoint)
.replyOnce(httpStatusCodes.OK, mockData);
});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 2c6b603197d..6afdab455a6 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -759,6 +759,19 @@ describe('URL utility', () => {
});
});
+ describe('cleanEndingSeparator', () => {
+ it.each`
+ path | expected
+ ${'foo/bar'} | ${'foo/bar'}
+ ${'/foo/bar/'} | ${'/foo/bar'}
+ ${'foo/bar//'} | ${'foo/bar'}
+ ${'foo/bar/./'} | ${'foo/bar/.'}
+ ${''} | ${''}
+ `('$path becomes $expected', ({ path, expected }) => {
+ expect(urlUtils.cleanEndingSeparator(path)).toBe(expected);
+ });
+ });
+
describe('joinPaths', () => {
it.each`
paths | expected
diff --git a/spec/frontend/listbox/index_spec.js b/spec/frontend/listbox/index_spec.js
index fd41531796b..0816152f4e3 100644
--- a/spec/frontend/listbox/index_spec.js
+++ b/spec/frontend/listbox/index_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
import { getAllByRole, getByTestId } from '@testing-library/dom';
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { createWrapper } from '@vue/test-utils';
import { initListbox, parseAttributes } from '~/listbox';
import { getFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
@@ -41,7 +41,7 @@ describe('initListbox', () => {
describe('given a valid element', () => {
let onChangeSpy;
- const listbox = () => createWrapper(instance).findComponent(GlListbox);
+ const listbox = () => createWrapper(instance).findComponent(GlCollapsibleListbox);
const findToggleButton = () => getByTestId(document.body, 'base-dropdown-toggle');
const findSelectedItems = () => getAllByRole(document.body, 'option', { selected: true });
diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
index 4580fdb06f2..f346967121c 100644
--- a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
+++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
@@ -9,6 +9,7 @@ import {
FILTERED_SEARCH_TOKEN_TWO_FACTOR,
FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS,
} from '~/members/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
jest.mock('~/lib/utils/url_utility', () => {
@@ -130,7 +131,7 @@ describe('MembersFilteredSearchBar', () => {
expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: {
data: 'foobar',
},
@@ -145,7 +146,7 @@ describe('MembersFilteredSearchBar', () => {
expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: {
data: 'foo bar baz',
},
@@ -174,7 +175,7 @@ describe('MembersFilteredSearchBar', () => {
findFilteredSearchBar().vm.$emit('onFilter', [
{ type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type, value: { data: 'enabled', operator: '=' } },
- { type: 'filtered-search-term', value: { data: 'foobar' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'foobar' } },
]);
expect(redirectTo).toHaveBeenCalledWith(
@@ -187,7 +188,7 @@ describe('MembersFilteredSearchBar', () => {
findFilteredSearchBar().vm.$emit('onFilter', [
{ type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type, value: { data: 'enabled', operator: '=' } },
- { type: 'filtered-search-term', value: { data: 'foo bar baz' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'foo bar baz' } },
]);
expect(redirectTo).toHaveBeenCalledWith(
@@ -202,7 +203,7 @@ describe('MembersFilteredSearchBar', () => {
findFilteredSearchBar().vm.$emit('onFilter', [
{ type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type, value: { data: 'enabled', operator: '=' } },
- { type: 'filtered-search-term', value: { data: 'foobar' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'foobar' } },
]);
expect(redirectTo).toHaveBeenCalledWith(
@@ -216,7 +217,7 @@ describe('MembersFilteredSearchBar', () => {
createComponent();
findFilteredSearchBar().vm.$emit('onFilter', [
- { type: 'filtered-search-term', value: { data: 'foobar' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'foobar' } },
]);
expect(redirectTo).toHaveBeenCalledWith('https://localhost/?search=foobar&tab=invited');
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index c6e90a4b20d..69ff5e47689 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -303,6 +303,7 @@ describe('MergeRequestTabs', () => {
const tabContent = document.createElement('div');
beforeEach(() => {
+ $.fn.renderGFM = jest.fn();
jest.spyOn(mainContent, 'getBoundingClientRect').mockReturnValue({ top: 10 });
jest.spyOn(tabContent, 'getBoundingClientRect').mockReturnValue({ top: 100 });
jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
diff --git a/spec/frontend/merge_requests/components/target_project_dropdown_spec.js b/spec/frontend/merge_requests/components/target_project_dropdown_spec.js
new file mode 100644
index 00000000000..3fddbe7ae21
--- /dev/null
+++ b/spec/frontend/merge_requests/components/target_project_dropdown_spec.js
@@ -0,0 +1,80 @@
+import { mount } from '@vue/test-utils';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import TargetProjectDropdown from '~/merge_requests/components/target_project_dropdown.vue';
+
+let wrapper;
+let mock;
+
+function factory() {
+ wrapper = mount(TargetProjectDropdown, {
+ provide: {
+ targetProjectsPath: '/gitlab-org/gitlab/target_projects',
+ currentProject: { value: 1, text: 'gitlab-org/gitlab' },
+ },
+ });
+}
+
+const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+
+describe('Merge requests target project dropdown component', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet('/gitlab-org/gitlab/target_projects').reply(200, [
+ {
+ id: 10,
+ name: 'Gitlab Test',
+ full_path: '/root/gitlab-test',
+ full_name: 'Administrator / Gitlab Test',
+ refs_url: '/root/gitlab-test/refs',
+ },
+ {
+ id: 1,
+ name: 'Gitlab Test',
+ full_path: '/gitlab-org/gitlab-test',
+ full_name: 'Gitlab Org / Gitlab Test',
+ refs_url: '/gitlab-org/gitlab-test/refs',
+ },
+ ]);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ it('creates hidden input with currentProject ID', () => {
+ factory();
+
+ expect(wrapper.find('[data-testid="target-project-input"]').attributes('value')).toBe('1');
+ });
+
+ it('renders list of projects', async () => {
+ factory();
+
+ wrapper.find('[data-testid="base-dropdown-toggle"]').trigger('click');
+
+ await waitForPromises();
+
+ expect(wrapper.findAll('li').length).toBe(2);
+ expect(wrapper.findAll('li').at(0).text()).toBe('root/gitlab-test');
+ expect(wrapper.findAll('li').at(1).text()).toBe('gitlab-org/gitlab-test');
+ });
+
+ it('searches projects', async () => {
+ factory();
+
+ wrapper.find('[data-testid="base-dropdown-toggle"]').trigger('click');
+
+ await waitForPromises();
+
+ findDropdown().vm.$emit('search', 'test');
+
+ jest.advanceTimersByTime(500);
+ await waitForPromises();
+
+ expect(mock.history.get[1].params).toEqual({ search: 'test' });
+ });
+});
diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js
index ce5b2a1000b..c20c51db75e 100644
--- a/spec/frontend/milestones/components/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/components/milestone_combobox_spec.js
@@ -346,7 +346,7 @@ describe('Milestone combobox component', () => {
expect(
findFirstProjectMilestonesDropdownItem()
.find('svg')
- .classes('gl-new-dropdown-item-check-icon'),
+ .classes('gl-dropdown-item-check-icon'),
).toBe(true);
selectFirstProjectMilestone();
@@ -473,7 +473,7 @@ describe('Milestone combobox component', () => {
expect(
findFirstGroupMilestonesDropdownItem()
.find('svg')
- .classes('gl-new-dropdown-item-check-icon'),
+ .classes('gl-dropdown-item-check-icon'),
).toBe(true);
selectFirstGroupMilestone();
diff --git a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap
new file mode 100644
index 00000000000..8af0753f929
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap
@@ -0,0 +1,233 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MlCandidate renders correctly 1`] = `
+<div>
+ <div
+ class="gl-alert gl-alert-warning"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16 gl-alert-icon"
+ data-testid="warning-icon"
+ role="img"
+ >
+ <use
+ href="#warning"
+ />
+ </svg>
+
+ <div
+ aria-live="assertive"
+ class="gl-alert-content"
+ role="alert"
+ >
+ <h2
+ class="gl-alert-title"
+ >
+ Machine Learning Experiment Tracking is in Incubating Phase
+ </h2>
+
+ <div
+ class="gl-alert-body"
+ >
+
+ GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited
+
+ <a
+ class="gl-link"
+ href="https://about.gitlab.com/handbook/engineering/incubation/"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ Learn more
+ </a>
+ </div>
+
+ <div
+ class="gl-alert-actions"
+ >
+ <a
+ class="btn gl-alert-action btn-confirm btn-md gl-button"
+ href="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Feedback
+
+ </span>
+ </a>
+ </div>
+ </div>
+
+ <button
+ aria-label="Dismiss"
+ class="btn gl-dismiss-btn btn-default btn-sm gl-button btn-default-tertiary btn-icon"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="close-icon"
+ role="img"
+ >
+ <use
+ href="#close"
+ />
+ </svg>
+
+ <!---->
+ </button>
+ </div>
+
+ <h3>
+
+ Model candidate details
+
+ </h3>
+
+ <table
+ class="candidate-details"
+ >
+ <tbody>
+ <tr
+ class="divider"
+ />
+
+ <tr>
+ <td
+ class="gl-text-secondary gl-font-weight-bold"
+ >
+ Info
+ </td>
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ ID
+ </td>
+
+ <td>
+ candidate_iid
+ </td>
+ </tr>
+
+ <tr>
+ <td />
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ Status
+ </td>
+
+ <td>
+ SUCCESS
+ </td>
+ </tr>
+
+ <tr>
+ <td />
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ Experiment
+ </td>
+
+ <td>
+ <a
+ class="gl-link"
+ href="#"
+ >
+ The Experiment
+ </a>
+ </td>
+ </tr>
+
+ <!---->
+
+ <tr
+ class="divider"
+ />
+
+ <tr>
+ <td
+ class="gl-text-secondary gl-font-weight-bold"
+ >
+
+ Parameters
+
+ </td>
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ Algorithm
+ </td>
+
+ <td>
+ Decision Tree
+ </td>
+ </tr>
+ <tr>
+ <td />
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ MaxDepth
+ </td>
+
+ <td>
+ 3
+ </td>
+ </tr>
+
+ <tr
+ class="divider"
+ />
+
+ <tr>
+ <td
+ class="gl-text-secondary gl-font-weight-bold"
+ >
+
+ Metrics
+
+ </td>
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ AUC
+ </td>
+
+ <td>
+ .55
+ </td>
+ </tr>
+ <tr>
+ <td />
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ Accuracy
+ </td>
+
+ <td>
+ .99
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+`;
diff --git a/spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap
index 2eba8869535..e253a0afc6c 100644
--- a/spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap
+++ b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ShowExperiment with candidates renders correctly 1`] = `
+exports[`MlExperiment with candidates renders correctly 1`] = `
<div>
<div
class="gl-alert gl-alert-warning"
@@ -39,7 +39,7 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
rel="noopener noreferrer"
target="_blank"
>
- Learn More
+ Learn more
</a>
</div>
@@ -48,7 +48,7 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
>
<a
class="btn gl-alert-action btn-confirm btn-md gl-button"
- href="https://gitlab.com/groups/gitlab-org/-/epics/8560"
+ href="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
>
<!---->
@@ -58,7 +58,7 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
class="gl-button-text"
>
- Feedback and Updates
+ Feedback
</span>
</a>
@@ -89,13 +89,13 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
<h3>
- Experiment Candidates
+ Experiment candidates
</h3>
<table
aria-busy="false"
- aria-colcount="4"
+ aria-colcount="6"
class="table b-table gl-table gl-mt-0!"
role="table"
>
@@ -150,6 +150,24 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
Mae
</div>
</th>
+ <th
+ aria-colindex="5"
+ aria-label="Details"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div />
+ </th>
+ <th
+ aria-colindex="6"
+ aria-label="Artifact"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div />
+ </th>
</tr>
</thead>
<tbody
@@ -184,6 +202,32 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
class=""
role="cell"
/>
+ <td
+ aria-colindex="5"
+ class=""
+ role="cell"
+ >
+ <a
+ class="gl-link"
+ href="link_to_candidate1"
+ >
+ Details
+ </a>
+ </td>
+ <td
+ aria-colindex="6"
+ class=""
+ role="cell"
+ >
+ <a
+ class="gl-link"
+ href="link_to_artifact"
+ rel="noopener"
+ target="_blank"
+ >
+ Artifacts
+ </a>
+ </td>
</tr>
<tr
class=""
@@ -213,6 +257,23 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
class=""
role="cell"
/>
+ <td
+ aria-colindex="5"
+ class=""
+ role="cell"
+ >
+ <a
+ class="gl-link"
+ href="link_to_candidate2"
+ >
+ Details
+ </a>
+ </td>
+ <td
+ aria-colindex="6"
+ class=""
+ role="cell"
+ />
</tr>
<!---->
<!---->
diff --git a/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js b/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js
index e07a4ed816b..7dca360c7ee 100644
--- a/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js
@@ -15,7 +15,7 @@ describe('IncubationAlert', () => {
it('displays link to issue', () => {
expect(findButton().attributes().href).toBe(
- 'https://gitlab.com/groups/gitlab-org/-/epics/8560',
+ 'https://gitlab.com/gitlab-org/gitlab/-/issues/381660',
);
});
diff --git a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js b/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
new file mode 100644
index 00000000000..4b16312815a
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
@@ -0,0 +1,43 @@
+import { GlAlert } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import MlCandidate from '~/ml/experiment_tracking/components/ml_candidate.vue';
+
+describe('MlCandidate', () => {
+ let wrapper;
+
+ const createWrapper = () => {
+ const candidate = {
+ params: [
+ { name: 'Algorithm', value: 'Decision Tree' },
+ { name: 'MaxDepth', value: '3' },
+ ],
+ metrics: [
+ { name: 'AUC', value: '.55' },
+ { name: 'Accuracy', value: '.99' },
+ ],
+ info: {
+ iid: 'candidate_iid',
+ artifact_link: 'path_to_artifact',
+ experiment_name: 'The Experiment',
+ experiment_path: 'path/to/experiment',
+ status: 'SUCCESS',
+ },
+ };
+
+ return mountExtended(MlCandidate, { provide: { candidate } });
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ it('shows incubation warning', () => {
+ wrapper = createWrapper();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('renders correctly', () => {
+ wrapper = createWrapper();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/components/experiment_spec.js b/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
index af722d77532..50539440f25 100644
--- a/spec/frontend/ml/experiment_tracking/components/experiment_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
@@ -1,17 +1,17 @@
import { GlAlert } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import ShowExperiment from '~/ml/experiment_tracking/components/experiment.vue';
+import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
-describe('ShowExperiment', () => {
+describe('MlExperiment', () => {
let wrapper;
const createWrapper = (candidates = [], metricNames = [], paramNames = []) => {
- return mountExtended(ShowExperiment, { provide: { candidates, metricNames, paramNames } });
+ return mountExtended(MlExperiment, { provide: { candidates, metricNames, paramNames } });
};
const findAlert = () => wrapper.findComponent(GlAlert);
- const findEmptyState = () => wrapper.findByText('This Experiment has no logged Candidates');
+ const findEmptyState = () => wrapper.findByText('This experiment has no logged candidates');
it('shows incubation warning', () => {
wrapper = createWrapper();
@@ -31,8 +31,8 @@ describe('ShowExperiment', () => {
it('renders correctly', () => {
wrapper = createWrapper(
[
- { rmse: 1, l1_ratio: 0.4 },
- { auc: 0.3, l1_ratio: 0.5 },
+ { rmse: 1, l1_ratio: 0.4, details: 'link_to_candidate1', artifact: 'link_to_artifact' },
+ { auc: 0.3, l1_ratio: 0.5, details: 'link_to_candidate2' },
],
['rmse', 'auc', 'mae'],
['l1_ratio'],
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index 263d6225a9f..3b4554700b4 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -3,7 +3,6 @@
exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="prometheus-graphs"
- data-qa-selector="prometheus_graphs_content"
data-testid="prometheus-graphs"
environmentstate="available"
metricsdashboardbasepath="/monitoring/monitor-project/-/metrics?environment=1"
@@ -40,7 +39,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
>
<dashboards-dropdown-stub
class="flex-grow-1"
- data-qa-selector="dashboards_filter_dropdown"
defaultbranch="master"
id="monitor-dashboards-dropdown"
toggle-class="dropdown-menu-toggle"
@@ -60,7 +58,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
class="flex-grow-1"
clearalltext="Clear all"
clearalltextclass="gl-px-5"
- data-qa-selector="environments_dropdown"
data-testid="environments-dropdown"
headertext=""
hideheaderborder="true"
@@ -106,7 +103,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<date-time-picker-stub
class="flex-grow-1 show-last-dropdown"
customenabled="true"
- data-qa-selector="range_picker_dropdown"
options="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
value="[object Object]"
/>
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
index e00736954a9..cb300870689 100644
--- a/spec/frontend/monitoring/components/refresh_button_spec.js
+++ b/spec/frontend/monitoring/components/refresh_button_spec.js
@@ -52,20 +52,6 @@ describe('RefreshButton', () => {
expect(findDropdown().props('text')).toBe('Off');
});
- describe('when feature flag disable_metric_dashboard_refresh_rate is on', () => {
- beforeEach(() => {
- createWrapper({
- provide: {
- glFeatures: { disableMetricDashboardRefreshRate: true },
- },
- });
- });
-
- it('refresh rate is not available', () => {
- expect(findDropdown().exists()).toBe(false);
- });
- });
-
describe('refresh rate options', () => {
it('presents multiple options', () => {
expect(findOptions().length).toBeGreaterThan(1);
diff --git a/spec/frontend/monitoring/requests/index_spec.js b/spec/frontend/monitoring/requests/index_spec.js
index 6f9af911a9f..def4bfe9443 100644
--- a/spec/frontend/monitoring/requests/index_spec.js
+++ b/spec/frontend/monitoring/requests/index_spec.js
@@ -2,7 +2,10 @@ import MockAdapter from 'axios-mock-adapter';
import { backoffMockImplementation } from 'helpers/backoff_helper';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
-import statusCodes from '~/lib/utils/http_status';
+import statusCodes, {
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+} from '~/lib/utils/http_status';
import { getDashboard, getPrometheusQueryData } from '~/monitoring/requests';
import { metricsDashboardResponse } from '../fixture_data';
@@ -37,8 +40,8 @@ describe('monitoring metrics_requests', () => {
});
it('returns a dashboard response after retrying twice', () => {
- mock.onGet(dashboardEndpoint).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(dashboardEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(dashboardEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
+ mock.onGet(dashboardEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(dashboardEndpoint).reply(statusCodes.OK, response);
return getDashboard(dashboardEndpoint, params).then((data) => {
@@ -81,8 +84,8 @@ describe('monitoring metrics_requests', () => {
it('returns a dashboard response after retrying twice', () => {
// Mock multiple attempts while the cache is filling up
- mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(prometheusEndpoint).reply(statusCodes.OK, response); // 3rd attempt
return getPrometheusQueryData(prometheusEndpoint, params).then((data) => {
@@ -116,8 +119,8 @@ describe('monitoring metrics_requests', () => {
it('rejects after retrying twice and getting an HTTP 500 error', () => {
// Mock multiple attempts while the cache is filling up and fails
- mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(prometheusEndpoint).reply(500, {
status: 'error',
error: 'An error occurred',
@@ -132,7 +135,7 @@ describe('monitoring metrics_requests', () => {
it.each`
code | reason
${statusCodes.BAD_REQUEST} | ${'Parameters are missing or incorrect'}
- ${statusCodes.UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
+ ${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
${statusCodes.SERVICE_UNAVAILABLE} | ${'Query timed out or aborted'}
`('rejects with details: "$reason" after getting an HTTP $code error', ({ code, reason }) => {
mock.onGet(prometheusEndpoint).reply(code, {
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index ca66768c3cc..93af6526c67 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -4,7 +4,10 @@ import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
-import statusCodes from '~/lib/utils/http_status';
+import statusCodes, {
+ HTTP_STATUS_CREATED,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+} from '~/lib/utils/http_status';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
import getAnnotations from '~/monitoring/queries/get_annotations.query.graphql';
@@ -944,7 +947,7 @@ describe('Monitoring store actions', () => {
});
it('Succesful POST request resolves', async () => {
- mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
+ mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_CREATED, {
dashboard: dashboardGitResponse[1],
});
@@ -969,7 +972,7 @@ describe('Monitoring store actions', () => {
commit_message: 'A new commit message',
});
- mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
+ mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_CREATED, {
dashboard: mockCreatedDashboard,
});
@@ -1133,7 +1136,7 @@ describe('Monitoring store actions', () => {
mock
.onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
- .reply(statusCodes.UNPROCESSABLE_ENTITY, {
+ .reply(HTTP_STATUS_UNPROCESSABLE_ENTITY, {
message: mockErrorMsg,
});
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
index 6c6c3d6b90f..348825c334a 100644
--- a/spec/frontend/monitoring/utils_spec.js
+++ b/spec/frontend/monitoring/utils_spec.js
@@ -435,6 +435,7 @@ describe('monitoring/utils', () => {
describe('setCustomVariablesFromUrl', () => {
beforeEach(() => {
+ window.history.pushState = jest.fn();
jest.spyOn(urlUtils, 'updateHistory');
});
diff --git a/spec/frontend/nav/components/new_nav_toggle_spec.js b/spec/frontend/nav/components/new_nav_toggle_spec.js
new file mode 100644
index 00000000000..f09bdef8caa
--- /dev/null
+++ b/spec/frontend/nav/components/new_nav_toggle_spec.js
@@ -0,0 +1,98 @@
+import { mount, createWrapper } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { getByText as getByTextHelper } from '@testing-library/dom';
+import { GlToggle } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import { s__ } from '~/locale';
+
+jest.mock('~/flash');
+
+const TEST_ENDPONT = 'https://example.com/toggle';
+
+describe('NewNavToggle', () => {
+ let wrapper;
+
+ const findToggle = () => wrapper.findComponent(GlToggle);
+
+ const createComponent = (propsData = { enabled: false }) => {
+ wrapper = mount(NewNavToggle, {
+ propsData: {
+ endpoint: TEST_ENDPONT,
+ ...propsData,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const getByText = (text, options) =>
+ createWrapper(getByTextHelper(wrapper.element, text, options));
+
+ it('renders its title', () => {
+ createComponent();
+ expect(getByText('Navigation redesign').exists()).toBe(true);
+ });
+
+ describe('when user preference is enabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: true });
+ });
+
+ it('renders the toggle as enabled', () => {
+ expect(findToggle().props('value')).toBe(true);
+ });
+ });
+
+ describe('when user preference is disabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: false });
+ });
+
+ it('renders the toggle as disabled', () => {
+ expect(findToggle().props('value')).toBe(false);
+ });
+ });
+
+ describe('changing the toggle', () => {
+ useMockLocationHelper();
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent();
+ });
+
+ it('reloads the page on success', async () => {
+ mock.onPut(TEST_ENDPONT).reply(200);
+ findToggle().vm.$emit('change');
+ await waitForPromises();
+
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+
+ it('shows an alert on error', async () => {
+ mock.onPut(TEST_ENDPONT).reply(500);
+ findToggle().vm.$emit('change');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: s__(
+ 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
+ ),
+ }),
+ );
+ expect(window.location.reload).not.toHaveBeenCalled();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index a74d709ed3a..add2ed1ba8a 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -1,6 +1,5 @@
import { getByRole } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils';
-import '~/behaviors/markdown/render_gfm';
import { nextTick } from 'vue';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableNote from '~/notes/components/noteable_note.vue';
@@ -11,6 +10,8 @@ import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_sys
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
const LINE_RANGE = {};
const DISCUSSION_WITH_LINE_RANGE = {
...discussionMock,
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index 2175849aeb9..a90d8bdde06 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -9,7 +9,6 @@ import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_i
import NoteForm from '~/notes/components/note_form.vue';
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
import createStore from '~/notes/stores';
-import '~/behaviors/markdown/render_gfm';
import {
noteableDataMock,
discussionMock,
@@ -18,6 +17,8 @@ import {
userDataMock,
} from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
describe('noteable_discussion component', () => {
let store;
let wrapper;
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 9051fcab97f..0c3d0da4f0f 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -2,23 +2,24 @@ import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { nextTick } from 'vue';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import axios from '~/lib/utils/axios_utils';
+import { getLocationHash } from '~/lib/utils/url_utility';
import * as urlUtility from '~/lib/utils/url_utility';
import CommentForm from '~/notes/components/comment_form.vue';
import NotesApp from '~/notes/components/notes_app.vue';
import NotesActivityHeader from '~/notes/components/notes_activity_header.vue';
import * as constants from '~/notes/constants';
import createStore from '~/notes/stores';
-import '~/behaviors/markdown/render_gfm';
-// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491)
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
+// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491)
import * as mockData from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
const TYPE_COMMENT_FORM = 'comment-form';
const TYPE_NOTES_LIST = 'notes-list';
const TEST_NOTES_FILTER_VALUE = 1;
@@ -26,7 +27,6 @@ const TEST_NOTES_FILTER_VALUE = 1;
const propsData = {
noteableData: mockData.noteableDataMock,
notesData: mockData.notesDataMock,
- userData: mockData.userDataMock,
notesFilters: mockData.notesFilters,
notesFilterValue: TEST_NOTES_FILTER_VALUE,
};
@@ -37,6 +37,19 @@ describe('note_app', () => {
let wrapper;
let store;
+ const initStore = (notesData = propsData.notesData) => {
+ store.dispatch('setNotesData', notesData);
+ store.dispatch('setNoteableData', propsData.noteableData);
+ store.dispatch('setUserData', mockData.userDataMock);
+ store.dispatch('setTargetNoteHash', getLocationHash());
+ // call after mounted hook
+ queueMicrotask(() => {
+ queueMicrotask(() => {
+ store.dispatch('fetchNotes');
+ });
+ });
+ };
+
const findCommentButton = () => wrapper.find('[data-testid="comment-button"]');
const getComponentOrder = () => {
@@ -51,7 +64,9 @@ describe('note_app', () => {
axiosMock = new AxiosMockAdapter(axios);
store = createStore();
+
mountComponent = ({ props = {} } = {}) => {
+ initStore();
return mount(
{
components: {
@@ -60,6 +75,7 @@ describe('note_app', () => {
template: `<div class="js-vue-notes-event">
<notes-app ref="notesApp" v-bind="$attrs" />
</div>`,
+ inheritAttrs: false,
},
{
propsData: {
@@ -77,53 +93,13 @@ describe('note_app', () => {
axiosMock.restore();
});
- describe('set data', () => {
- beforeEach(() => {
- setHTMLFixture('<div class="js-discussions-count"></div>');
-
- axiosMock.onAny().reply(200, []);
- wrapper = mountComponent();
- return waitForPromises();
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('should set notes data', () => {
- expect(store.state.notesData).toEqual(mockData.notesDataMock);
- });
-
- it('should set issue data', () => {
- expect(store.state.noteableData).toEqual(mockData.noteableDataMock);
- });
-
- it('should set user data', () => {
- expect(store.state.userData).toEqual(mockData.userDataMock);
- });
-
- it('should fetch discussions', () => {
- expect(store.state.discussions).toEqual([]);
- });
-
- it('updates discussions badge', () => {
- expect(document.querySelector('.js-discussions-count').textContent).toEqual('0');
- });
- });
-
describe('render', () => {
beforeEach(() => {
- setHTMLFixture('<div class="js-discussions-count"></div>');
-
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
wrapper = mountComponent();
return waitForPromises();
});
- afterEach(() => {
- resetHTMLFixture();
- });
-
it('should render list of notes', () => {
const note =
mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
@@ -148,10 +124,6 @@ describe('note_app', () => {
expect(findCommentButton().props('disabled')).toEqual(true);
});
- it('updates discussions badge', () => {
- expect(document.querySelector('.js-discussions-count').textContent).toEqual('2');
- });
-
it('should render notes activity header', () => {
expect(wrapper.findComponent(NotesActivityHeader).props()).toEqual({
notesFilterValue: TEST_NOTES_FILTER_VALUE,
@@ -162,8 +134,6 @@ describe('note_app', () => {
describe('render with comments disabled', () => {
beforeEach(() => {
- setHTMLFixture('<div class="js-discussions-count"></div>');
-
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
wrapper = mountComponent({
// why: In this integration test, previously we manually set store.state.commentsDisabled
@@ -177,10 +147,6 @@ describe('note_app', () => {
return waitForPromises();
});
- afterEach(() => {
- resetHTMLFixture();
- });
-
it('should not render form when commenting is disabled', () => {
expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
});
@@ -192,8 +158,6 @@ describe('note_app', () => {
describe('timeline view', () => {
beforeEach(() => {
- setHTMLFixture('<div class="js-discussions-count"></div>');
-
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
store.state.commentsDisabled = false;
store.state.isTimelineEnabled = true;
@@ -202,10 +166,6 @@ describe('note_app', () => {
return waitForPromises();
});
- afterEach(() => {
- resetHTMLFixture();
- });
-
it('should not render comments form', () => {
expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
});
@@ -213,14 +173,9 @@ describe('note_app', () => {
describe('while fetching data', () => {
beforeEach(async () => {
- setHTMLFixture('<div class="js-discussions-count"></div>');
wrapper = mountComponent();
});
- afterEach(() => {
- return waitForPromises().then(() => resetHTMLFixture());
- });
-
it('renders skeleton notes', () => {
expect(wrapper.find('.gl-skeleton-loader-default-container').exists()).toBe(true);
});
@@ -231,10 +186,6 @@ describe('note_app', () => {
'Write a comment or drag your files here…',
);
});
-
- it('should not update discussions badge (it should be blank)', () => {
- expect(document.querySelector('.js-discussions-count').textContent).toEqual('');
- });
});
describe('update note', () => {
@@ -468,7 +419,9 @@ describe('note_app', () => {
describe('fetching discussions', () => {
describe('when note anchor is not present', () => {
it('does not include extra query params', async () => {
- wrapper = shallowMount(NotesApp, { propsData, store: createStore() });
+ store = createStore();
+ initStore();
+ wrapper = shallowMount(NotesApp, { propsData, store });
await waitForPromises();
expect(axiosMock.history.get[0].params).toEqual({ per_page: 20 });
@@ -476,17 +429,16 @@ describe('note_app', () => {
});
describe('when note anchor is present', () => {
- const mountWithNotesFilter = (notesFilter) =>
- shallowMount(NotesApp, {
- propsData: {
- ...propsData,
- notesData: {
- ...propsData.notesData,
- notesFilter,
- },
- },
+ const mountWithNotesFilter = (notesFilter) => {
+ initStore({
+ ...propsData.notesData,
+ notesFilter,
+ });
+ return shallowMount(NotesApp, {
+ propsData,
store: createStore(),
});
+ };
beforeEach(() => {
setWindowLocation('#note_1');
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index d5e2a189afe..f52c3e28691 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -4,7 +4,6 @@ import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { createSpyObj } from 'helpers/jest_helpers';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
@@ -254,16 +253,20 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
note: 'heya',
html: '<div>heya</div>',
};
- $notesList = createSpyObj('$notesList', ['find', 'append']);
-
- notes = createSpyObj('notes', [
- 'setupNewNote',
- 'refresh',
- 'collapseLongCommitList',
- 'updateNotesCount',
- 'putConflictEditWarningInPlace',
- ]);
- notes.taskList = createSpyObj('tasklist', ['init']);
+ $notesList = {
+ find: jest.fn(),
+ append: jest.fn(),
+ };
+ notes = {
+ setupNewNote: jest.fn(),
+ refresh: jest.fn(),
+ collapseLongCommitList: jest.fn(),
+ updateNotesCount: jest.fn(),
+ putConflictEditWarningInPlace: jest.fn(),
+ };
+ notes.taskList = {
+ init: jest.fn(),
+ };
notes.note_ids = [];
notes.updatedNotesTrackingMap = {};
@@ -383,11 +386,21 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
discussion_resolvable: false,
diff_discussion_html: false,
};
- $form = createSpyObj('$form', ['closest', 'find']);
+ $form = {
+ closest: jest.fn(),
+ find: jest.fn(),
+ };
$form.length = 1;
- row = createSpyObj('row', ['prevAll', 'first', 'find']);
+ row = {
+ prevAll: jest.fn(),
+ first: jest.fn(),
+ find: jest.fn(),
+ };
- notes = createSpyObj('notes', ['isParallelView', 'updateNotesCount']);
+ notes = {
+ isParallelView: jest.fn(),
+ updateNotesCount: jest.fn(),
+ };
notes.note_ids = [];
jest.spyOn(Notes, 'isNewNote');
@@ -403,7 +416,9 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
let body;
beforeEach(() => {
- body = createSpyObj('body', ['attr']);
+ body = {
+ attr: jest.fn(),
+ };
discussionContainer = { length: 0 };
$form.closest.mockReturnValueOnce(row).mockReturnValue($form);
@@ -462,7 +477,9 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
beforeEach(() => {
noteHTML = '<div></div>';
- $notesList = createSpyObj('$notesList', ['append']);
+ $notesList = {
+ append: jest.fn(),
+ };
$resultantNote = Notes.animateAppendNote(noteHTML, $notesList);
});
@@ -483,7 +500,9 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
beforeEach(() => {
noteHTML = '<div></div>';
- $note = createSpyObj('$note', ['replaceWith']);
+ $note = {
+ replaceWith: jest.fn(),
+ };
$updatedNote = Notes.animateUpdateNote(noteHTML, $note);
});
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 989dd74b6d0..dce2e5d370d 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -3,7 +3,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
@@ -13,8 +13,8 @@ import * as actions from '~/notes/stores/actions';
import * as mutationTypes from '~/notes/stores/mutation_types';
import mutations from '~/notes/stores/mutations';
import * as utils from '~/notes/stores/utils';
-import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
-import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
+import updateIssueLockMutation from '~/sidebar/queries/update_issue_lock.mutation.graphql';
+import updateMergeRequestLockMutation from '~/sidebar/queries/update_merge_request_lock.mutation.graphql';
import promoteTimelineEvent from '~/notes/graphql/promote_timeline_event.mutation.graphql';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
import notesEventHub from '~/notes/event_hub';
@@ -30,16 +30,12 @@ import {
} from '../mock_data';
const TEST_ERROR_MESSAGE = 'Test error message';
-const mockFlashClose = jest.fn();
-jest.mock('~/flash', () => {
- const flash = jest.fn().mockImplementation(() => {
- return {
- close: mockFlashClose,
- };
- });
-
- return flash;
-});
+const mockAlertDismiss = jest.fn();
+jest.mock('~/flash', () => ({
+ createAlert: jest.fn().mockImplementation(() => ({
+ dismiss: mockAlertDismiss,
+ })),
+}));
jest.mock('~/vue_shared/plugins/global_toast');
@@ -331,13 +327,13 @@ describe('Actions Notes Store', () => {
await startPolling();
expect(axiosMock.history.get).toHaveLength(1);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
await advanceXMoreIntervals(1);
expect(axiosMock.history.get).toHaveLength(2);
- expect(createFlash).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
it('resets the failure counter on success', async () => {
@@ -358,14 +354,13 @@ describe('Actions Notes Store', () => {
await advanceXMoreIntervals(1); // Failure #2
// That was the first failure AFTER a success, so we should NOT see the error displayed
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
// Now we'll allow another failure
await advanceXMoreIntervals(1); // Failure #3
// Since this is the second failure in a row, the error should happen
- expect(createFlash).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
it('hides the error display if it exists on success', async () => {
@@ -375,16 +370,14 @@ describe('Actions Notes Store', () => {
await advanceXMoreIntervals(2);
// After two errors, the error should be displayed
- expect(createFlash).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
axiosMock.reset();
successMock();
await advanceXMoreIntervals(1);
- expect(mockFlashClose).toHaveBeenCalled();
- expect(mockFlashClose).toHaveBeenCalledTimes(1);
+ expect(mockAlertDismiss).toHaveBeenCalledTimes(1);
});
});
});
@@ -869,7 +862,7 @@ describe('Actions Notes Store', () => {
payload,
),
).rejects.toEqual(error);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -885,8 +878,8 @@ describe('Actions Notes Store', () => {
},
{ ...payload, flashContainer },
);
- expect(resp.hasFlash).toBe(true);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(resp.hasAlert).toBe(true);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Your comment could not be submitted because something went wrong',
parent: flashContainer,
});
@@ -905,7 +898,7 @@ describe('Actions Notes Store', () => {
payload,
);
expect(data).toBe(res);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
});
@@ -943,7 +936,7 @@ describe('Actions Notes Store', () => {
['resolveDiscussion', { discussionId }],
['restartPolling'],
]);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -958,7 +951,7 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: TEST_ERROR_MESSAGE,
parent: flashContainer,
});
@@ -976,7 +969,7 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while applying the suggestion. Please try again.',
parent: flashContainer,
});
@@ -987,7 +980,7 @@ describe('Actions Notes Store', () => {
dispatch.mockReturnValue(Promise.reject());
return testSubmitSuggestion(() => {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
});
@@ -1029,7 +1022,7 @@ describe('Actions Notes Store', () => {
['restartPolling'],
]);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -1047,7 +1040,7 @@ describe('Actions Notes Store', () => {
]);
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: TEST_ERROR_MESSAGE,
parent: flashContainer,
});
@@ -1068,7 +1061,7 @@ describe('Actions Notes Store', () => {
]);
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message:
'Something went wrong while applying the batch of suggestions. Please try again.',
parent: flashContainer,
@@ -1088,7 +1081,7 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
});
@@ -1234,7 +1227,7 @@ describe('Actions Notes Store', () => {
),
).rejects.toEqual(new Error());
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
});
@@ -1414,7 +1407,7 @@ describe('Actions Notes Store', () => {
return actions
.promoteCommentToTimelineEvent({ commit: commitSpy }, actionArgs)
.then(() => {
- expect(createFlash).toHaveBeenCalledWith(expectedAlertArgs);
+ expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
expect(commitSpy).toHaveBeenCalledWith(
mutationTypes.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS,
false,
diff --git a/spec/frontend/observability/observability_app_spec.js b/spec/frontend/observability/observability_app_spec.js
index f0b318e69ec..248b0a2057c 100644
--- a/spec/frontend/observability/observability_app_spec.js
+++ b/spec/frontend/observability/observability_app_spec.js
@@ -1,5 +1,16 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ObservabilityApp from '~/observability/components/observability_app.vue';
+import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
+
+import {
+ MESSAGE_EVENT_TYPE,
+ OBSERVABILITY_ROUTES,
+ SKELETON_VARIANT,
+} from '~/observability/constants';
+
+import { darkModeEnabled } from '~/lib/utils/color_utils';
+
+jest.mock('~/lib/utils/color_utils');
describe('Observability root app', () => {
let wrapper;
@@ -12,6 +23,8 @@ describe('Observability root app', () => {
query: { otherQuery: 100 },
};
+ const mockHandleSkeleton = jest.fn();
+
const findIframe = () => wrapper.findByTestId('observability-ui-iframe');
const TEST_IFRAME_SRC = 'https://observe.gitlab.com/9970/?groupId=14485840';
@@ -21,6 +34,9 @@ describe('Observability root app', () => {
propsData: {
observabilityIframeSrc: TEST_IFRAME_SRC,
},
+ stubs: {
+ 'observability-skeleton': ObservabilitySkeleton,
+ },
mocks: {
$router,
$route: route,
@@ -28,46 +44,156 @@ describe('Observability root app', () => {
});
};
+ const dispatchMessageEvent = (message) =>
+ window.dispatchEvent(new MessageEvent('message', message));
+
afterEach(() => {
wrapper.destroy();
});
- it('should render an iframe with observabilityIframeSrc as src', () => {
- mountComponent();
- const iframe = findIframe();
- expect(iframe.exists()).toBe(true);
- expect(iframe.attributes('src')).toBe(TEST_IFRAME_SRC);
+ describe('iframe src', () => {
+ const TEST_USERNAME = 'test-user';
+
+ beforeAll(() => {
+ gon.current_username = TEST_USERNAME;
+ });
+
+ it('should render an iframe with observabilityIframeSrc, decorated with light theme and username', () => {
+ darkModeEnabled.mockReturnValueOnce(false);
+ mountComponent();
+ const iframe = findIframe();
+
+ expect(iframe.exists()).toBe(true);
+ expect(iframe.attributes('src')).toBe(
+ `${TEST_IFRAME_SRC}&theme=light&username=${TEST_USERNAME}`,
+ );
+ });
+
+ it('should render an iframe with observabilityIframeSrc decorated with dark theme and username', () => {
+ darkModeEnabled.mockReturnValueOnce(true);
+ mountComponent();
+ const iframe = findIframe();
+
+ expect(iframe.exists()).toBe(true);
+ expect(iframe.attributes('src')).toBe(
+ `${TEST_IFRAME_SRC}&theme=dark&username=${TEST_USERNAME}`,
+ );
+ });
});
- it('should not call replace method from vue router if message event does not have url', () => {
- mountComponent();
- wrapper.vm.messageHandler({ data: 'some other data' });
- expect(replace).not.toHaveBeenCalled();
+ describe('iframe sandbox', () => {
+ it('should render an iframe with sandbox attributes', () => {
+ mountComponent();
+ const iframe = findIframe();
+
+ expect(iframe.exists()).toBe(true);
+ expect(iframe.attributes('sandbox')).toBe('allow-same-origin allow-forms allow-scripts');
+ });
});
- it.each`
- condition | origin | observability_path | url
- ${'message origin is different from iframe source origin'} | ${'https://example.com'} | ${'/'} | ${'/explore'}
- ${'path is same as before (observability_path)'} | ${'https://observe.gitlab.com'} | ${'/foo?bar=test'} | ${'/foo?bar=test'}
- `(
- 'should not call replace method from vue router if $condition',
- async ({ origin, observability_path, url }) => {
- mountComponent({ ...$route, query: { observability_path } });
- wrapper.vm.messageHandler({ data: { url }, origin });
+ describe('on GOUI_ROUTE_UPDATE', () => {
+ it('should not call replace method from vue router if message event does not have url', () => {
+ mountComponent();
+ dispatchMessageEvent({
+ type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE,
+ payload: { data: 'some other data' },
+ });
expect(replace).not.toHaveBeenCalled();
- },
- );
-
- it('should call replace method from vue router on messageHandle call', () => {
- mountComponent();
- wrapper.vm.messageHandler({ data: { url: '/explore' }, origin: 'https://observe.gitlab.com' });
- expect(replace).toHaveBeenCalled();
- expect(replace).toHaveBeenCalledWith({
- name: 'https://gitlab.com/gitlab-org/',
- query: {
- otherQuery: 100,
- observability_path: '/explore',
+ });
+
+ it.each`
+ condition | origin | observability_path | url
+ ${'message origin is different from iframe source origin'} | ${'https://example.com'} | ${'/'} | ${'/explore'}
+ ${'path is same as before (observability_path)'} | ${'https://observe.gitlab.com'} | ${'/foo?bar=test'} | ${'/foo?bar=test'}
+ `(
+ 'should not call replace method from vue router if $condition',
+ async ({ origin, observability_path, url }) => {
+ mountComponent({ ...$route, query: { observability_path } });
+ dispatchMessageEvent({
+ data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url } },
+ origin,
+ });
+ expect(replace).not.toHaveBeenCalled();
},
+ );
+
+ it('should call replace method from vue router on message event callback', () => {
+ mountComponent();
+
+ dispatchMessageEvent({
+ data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url: '/explore' } },
+ origin: 'https://observe.gitlab.com',
+ });
+
+ expect(replace).toHaveBeenCalled();
+ expect(replace).toHaveBeenCalledWith({
+ name: 'https://gitlab.com/gitlab-org/',
+ query: {
+ otherQuery: 100,
+ observability_path: '/explore',
+ },
+ });
+ });
+ });
+
+ describe('on GOUI_LOADED', () => {
+ beforeEach(() => {
+ mountComponent();
+ wrapper.vm.$refs.iframeSkeleton.handleSkeleton = mockHandleSkeleton;
+ });
+ it('should call handleSkeleton method', () => {
+ dispatchMessageEvent({
+ data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
+ origin: 'https://observe.gitlab.com',
+ });
+ expect(mockHandleSkeleton).toHaveBeenCalled();
+ });
+
+ it('should not call handleSkeleton method if origin is different', () => {
+ dispatchMessageEvent({
+ data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
+ origin: 'https://example.com',
+ });
+ expect(mockHandleSkeleton).not.toHaveBeenCalled();
+ });
+
+ it('should not call handleSkeleton method if event type is different', () => {
+ dispatchMessageEvent({
+ data: { type: 'UNKNOWN_EVENT' },
+ origin: 'https://observe.gitlab.com',
+ });
+ expect(mockHandleSkeleton).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('skeleton variant', () => {
+ it.each`
+ pathDescription | path | variant
+ ${'dashboards'} | ${OBSERVABILITY_ROUTES.DASHBOARDS} | ${SKELETON_VARIANT.DASHBOARDS}
+ ${'explore'} | ${OBSERVABILITY_ROUTES.EXPLORE} | ${SKELETON_VARIANT.EXPLORE}
+ ${'manage dashboards'} | ${OBSERVABILITY_ROUTES.MANAGE} | ${SKELETON_VARIANT.MANAGE}
+ ${'any other'} | ${'unknown/route'} | ${SKELETON_VARIANT.DASHBOARDS}
+ `('renders the $variant skeleton variant for $pathDescription path', ({ path, variant }) => {
+ mountComponent({ ...$route, path });
+ const props = wrapper.findComponent(ObservabilitySkeleton).props();
+
+ expect(props.variant).toBe(variant);
+ });
+ });
+
+ describe('on observability ui unmount', () => {
+ it('should remove message event and should not call replace method from vue router', () => {
+ mountComponent();
+ wrapper.destroy();
+
+ // testing event cleanup logic, should not call on messege event after component is destroyed
+
+ dispatchMessageEvent({
+ data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url: '/explore' } },
+ origin: 'https://observe.gitlab.com',
+ });
+
+ expect(replace).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/observability/skeleton_spec.js b/spec/frontend/observability/skeleton_spec.js
new file mode 100644
index 00000000000..5637c0e6d70
--- /dev/null
+++ b/spec/frontend/observability/skeleton_spec.js
@@ -0,0 +1,96 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
+import DashboardsSkeleton from '~/observability/components/skeleton/dashboards.vue';
+import ExploreSkeleton from '~/observability/components/skeleton/explore.vue';
+import ManageSkeleton from '~/observability/components/skeleton/manage.vue';
+
+import { SKELETON_VARIANT } from '~/observability/constants';
+
+describe('ObservabilitySkeleton component', () => {
+ let wrapper;
+
+ const mountComponent = ({ ...props } = {}) => {
+ wrapper = shallowMountExtended(ObservabilitySkeleton, {
+ propsData: props,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('on mount', () => {
+ beforeEach(() => {
+ jest.spyOn(global, 'setTimeout');
+ mountComponent();
+ });
+
+ it('should call setTimeout on mount and show ObservabilitySkeleton if Observability UI is not loaded yet', () => {
+ jest.runAllTimers();
+
+ expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 500);
+ expect(wrapper.vm.loading).toBe(true);
+ expect(wrapper.vm.timerId).not.toBeNull();
+ });
+
+ it('should call setTimeout on mount and dont show ObservabilitySkeleton if Observability UI is loaded', () => {
+ wrapper.vm.loading = false;
+ jest.runAllTimers();
+
+ expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 500);
+ expect(wrapper.vm.loading).toBe(false);
+ expect(wrapper.vm.timerId).not.toBeNull();
+ });
+ });
+
+ describe('handleSkeleton', () => {
+ it('will not show the skeleton if Observability UI is loaded before', () => {
+ jest.spyOn(global, 'clearTimeout');
+ mountComponent();
+ wrapper.vm.handleSkeleton();
+ expect(clearTimeout).toHaveBeenCalledWith(wrapper.vm.timerId);
+ });
+
+ it('will hide skeleton gracefully after 400ms if skeleton was present on screen before Observability UI', () => {
+ jest.spyOn(global, 'setTimeout');
+ mountComponent();
+ jest.runAllTimers();
+ wrapper.vm.handleSkeleton();
+ jest.runAllTimers();
+
+ expect(setTimeout).toHaveBeenCalledWith(wrapper.vm.hideSkeleton, 400);
+ expect(wrapper.vm.loading).toBe(false);
+ });
+ });
+
+ describe('skeleton variant', () => {
+ it.each`
+ skeletonType | condition | variant
+ ${'dashboards'} | ${'variant is dashboards'} | ${SKELETON_VARIANT.DASHBOARDS}
+ ${'explore'} | ${'variant is explore'} | ${SKELETON_VARIANT.EXPLORE}
+ ${'manage'} | ${'variant is manage'} | ${SKELETON_VARIANT.MANAGE}
+ ${'default'} | ${'variant is not manage, dashboards or explore'} | ${'unknown'}
+ `('should render $skeletonType skeleton if $condition', async ({ skeletonType, variant }) => {
+ mountComponent({ variant });
+ const showsDefaultSkeleton = ![
+ SKELETON_VARIANT.DASHBOARDS,
+ SKELETON_VARIANT.EXPLORE,
+ SKELETON_VARIANT.MANAGE,
+ ].includes(variant);
+ expect(wrapper.findComponent(DashboardsSkeleton).exists()).toBe(
+ skeletonType === SKELETON_VARIANT.DASHBOARDS,
+ );
+ expect(wrapper.findComponent(ExploreSkeleton).exists()).toBe(
+ skeletonType === SKELETON_VARIANT.EXPLORE,
+ );
+ expect(wrapper.findComponent(ManageSkeleton).exists()).toBe(
+ skeletonType === SKELETON_VARIANT.MANAGE,
+ );
+
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(showsDefaultSkeleton);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
index 4a026f35822..d45b993b5a2 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
@@ -6,7 +6,6 @@ import {
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
- DETAILS_IMPORTING_ERROR_MESSAGE,
ADMIN_GARBAGE_COLLECTION_TIP,
} from '~/packages_and_registries/container_registry/explorer/constants';
@@ -77,7 +76,6 @@ describe('Delete alert', () => {
});
});
});
-
describe('error states', () => {
describe.each`
deleteAlertType | message
@@ -107,25 +105,6 @@ describe('Delete alert', () => {
});
});
- describe('importing repository error state', () => {
- beforeEach(() => {
- mountComponent({
- deleteAlertType: 'danger_importing',
- containerRegistryImportingHelpPagePath: 'https://foobar',
- });
- });
-
- it('alert exist and text is appropriate', () => {
- expect(findAlert().text()).toMatchInterpolatedText(DETAILS_IMPORTING_ERROR_MESSAGE);
- });
-
- it('alert body contains link', () => {
- const alertLink = findLink();
- expect(alertLink.exists()).toBe(true);
- expect(alertLink.attributes('href')).toBe('https://foobar');
- });
- });
-
describe('dismissing alert', () => {
it('GlAlert dismiss event triggers a change event', () => {
mountComponent({ deleteAlertType: 'success_tags' });
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index b163557618e..1017ff06a25 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -18,7 +18,7 @@ import {
NO_TAGS_MATCHING_FILTERS_TITLE,
NO_TAGS_MATCHING_FILTERS_DESCRIPTION,
} from '~/packages_and_registries/container_registry/explorer/constants/index';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import { tagsMock, imageTagsMock, tagsPageInfo } from '../../mock_data';
describe('Tags List', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
index b11048cd7a2..e5b99f15e8c 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
@@ -249,15 +249,6 @@ export const graphQLDeleteImageRepositoryTagsMock = {
},
};
-export const graphQLDeleteImageRepositoryTagImportingErrorMock = {
- data: {
- destroyContainerRepositoryTags: {
- errors: ['repository importing'],
- __typename: 'DestroyContainerRepositoryTagsPayload',
- },
- },
-};
-
export const dockerCommands = {
dockerBuildCommand: 'foofoo',
dockerPushCommand: 'barbar',
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index 310398b01cf..26f0e506829 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -18,7 +18,6 @@ import {
UNFINISHED_STATUS,
DELETE_SCHEDULED,
ALERT_DANGER_IMAGE,
- ALERT_DANGER_IMPORTING,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
MISSING_OR_DELETED_IMAGE_TITLE,
MISSING_OR_DELETED_IMAGE_MESSAGE,
@@ -34,7 +33,6 @@ import Tracking from '~/tracking';
import {
graphQLImageDetailsMock,
graphQLDeleteImageRepositoryTagsMock,
- graphQLDeleteImageRepositoryTagImportingErrorMock,
graphQLProjectImageRepositoriesDetailsMock,
containerRepositoryMock,
graphQLEmptyImageDetailsMock,
@@ -341,7 +339,6 @@ describe('Details Page', () => {
const config = {
isAdmin: true,
garbageCollectionHelpPagePath: 'baz',
- containerRegistryImportingHelpPagePath: 'https://foobar',
};
const deleteAlertType = 'success_tag';
@@ -366,38 +363,6 @@ describe('Details Page', () => {
expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType });
});
-
- describe('importing repository error', () => {
- let mutationResolver;
- let tagsResolver;
- let detailsResolver;
-
- beforeEach(async () => {
- mutationResolver = jest
- .fn()
- .mockResolvedValue(graphQLDeleteImageRepositoryTagImportingErrorMock);
- tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock()));
- detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
-
- mountComponent({ mutationResolver, tagsResolver, detailsResolver });
- await waitForApolloRequestRender();
- });
-
- it('displays the proper alert', async () => {
- findTagsList().vm.$emit('delete', [cleanTags[0]]);
- await nextTick();
-
- findDeleteModal().vm.$emit('confirmDelete');
- await waitForPromises();
-
- expect(tagsResolver).toHaveBeenCalled();
- expect(detailsResolver).toHaveBeenCalled();
-
- const deleteAlert = findDeleteAlert();
- expect(deleteAlert.exists()).toBe(true);
- expect(deleteAlert.props('deleteAlertType')).toBe(ALERT_DANGER_IMPORTING);
- });
- });
});
describe('Partial Cleanup Alert', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
index 79403d29d18..1e514d85e82 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
@@ -6,7 +6,6 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import DeleteImage from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
import CliCommands from '~/packages_and_registries/shared/components/cli_commands.vue';
import GroupEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue';
@@ -23,6 +22,7 @@ import getContainerRepositoriesDetails from '~/packages_and_registries/container
import component from '~/packages_and_registries/container_registry/explorer/pages/list.vue';
import Tracking from '~/tracking';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { $toast } from 'jest/packages_and_registries/shared/mocks';
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index fb50d623543..329cc15df97 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -14,7 +14,6 @@ import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { stripTypenames } from 'helpers/graphql_helpers';
import waitForPromises from 'helpers/wait_for_promises';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import axios from '~/lib/utils/axios_utils';
@@ -190,7 +189,7 @@ describe('DependencyProxyApp', () => {
it('shows list', () => {
expect(findManifestList().props()).toMatchObject({
manifests: proxyManifests(),
- pagination: stripTypenames(pagination()),
+ pagination: pagination(),
});
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
index 9e4c747a1bd..2f415bfd6f9 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
@@ -1,5 +1,4 @@
import { GlKeysetPagination } from '@gitlab/ui';
-import { stripTypenames } from 'helpers/graphql_helpers';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ManifestRow from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
@@ -14,7 +13,7 @@ describe('Manifests List', () => {
const defaultProps = {
manifests: proxyManifests(),
- pagination: stripTypenames(pagination()),
+ pagination: pagination(),
};
const createComponent = (propsData = defaultProps) => {
@@ -60,9 +59,8 @@ describe('Manifests List', () => {
it('has the correct props', () => {
createComponent();
- expect(findPagination().props()).toMatchObject({
- ...defaultProps.pagination,
- });
+ const { __typename, ...paginationProps } = defaultProps.pagination;
+ expect(findPagination().props()).toMatchObject(paginationProps);
});
it('emits the next-page event', () => {
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
index 8fd50bea280..69765d31674 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
@@ -8,7 +8,7 @@ import ArtifactsList from '~/packages_and_registries/harbor_registry/components/
import waitForPromises from 'helpers/wait_for_promises';
import DetailsHeader from '~/packages_and_registries/harbor_registry/components/details/details_header.vue';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import {
NAME_SORT_FIELD,
TOKEN_TYPE_TAG_NAME,
@@ -137,7 +137,7 @@ describe('Harbor Details Page', () => {
title: s__('HarborRegistry|Tag'),
unique: true,
token: GlFilteredSearchToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
},
],
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
index dff95364d7d..d237023d0cd 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
@@ -7,13 +7,11 @@ import { createAlert, VARIANT_INFO } from '~/flash';
import * as commonUtils from '~/lib/utils/common_utils';
import PackageListApp from '~/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/infrastructure_registry/list/constants';
-import {
- SHOW_DELETE_SUCCESS_ALERT,
- FILTERED_SEARCH_TERM,
-} from '~/packages_and_registries/shared/constants';
+import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants';
import * as packageUtils from '~/packages_and_registries/shared/utils';
import InfrastructureSearch from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
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 92c2cd90568..c4020eeb75f 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
@@ -13,7 +13,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
<div>
<div
- class="dropdown b-dropdown gl-new-dropdown btn-group"
+ class="dropdown b-dropdown gl-dropdown btn-group"
id="__BVID__27"
lazy=""
>
@@ -30,7 +30,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
<!---->
<span
- class="gl-new-dropdown-button-text"
+ class="gl-dropdown-button-text"
>
Show PyPi commands
</span>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index 913b4f5926f..bb04701a8b7 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -1,4 +1,4 @@
-import { GlFormCheckbox, GlSprintf } from '@gitlab/ui';
+import { GlFormCheckbox, GlSprintf, GlTruncate } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -15,7 +15,13 @@ import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry/constants';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
-import { packageData, packagePipelines, packageProject, packageTags } from '../../mock_data';
+import {
+ linksData,
+ packageData,
+ packagePipelines,
+ packageProject,
+ packageTags,
+} from '../../mock_data';
Vue.use(VueRouter);
@@ -26,9 +32,9 @@ describe('packages_list_row', () => {
isGroupPage: false,
};
- const packageWithoutTags = { ...packageData(), project: packageProject() };
+ const packageWithoutTags = { ...packageData(), project: packageProject(), ...linksData };
const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } };
- const packageCannotDestroy = { ...packageData(), canDestroy: false };
+ const packageCannotDestroy = { ...packageData(), ...linksData, canDestroy: false };
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPackagePath = () => wrapper.findComponent(PackagePath);
@@ -41,6 +47,7 @@ describe('packages_list_row', () => {
const findCreatedDateText = () => wrapper.findByTestId('created-date');
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
+ const findPackageName = () => wrapper.findComponent(GlTruncate);
const mountComponent = ({
packageEntity = packageWithoutTags,
@@ -81,6 +88,22 @@ describe('packages_list_row', () => {
});
});
+ it('does not have a link to navigate to the details page', () => {
+ mountComponent({
+ packageEntity: {
+ ...packageWithoutTags,
+ _links: {
+ webPath: null,
+ },
+ },
+ });
+
+ expect(findPackageLink().exists()).toBe(false);
+ expect(findPackageName().props()).toMatchObject({
+ text: '@gitlab-org/package-15',
+ });
+ });
+
describe('tags', () => {
it('renders package tags when a package has tags', () => {
mountComponent({ packageEntity: packageWithTags });
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
index 19505618ff7..a884959ab62 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
@@ -10,6 +10,7 @@ import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { LIST_KEY_CREATED_AT } from '~/packages_and_registries/package_registry/constants';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
+import { TOKEN_TYPE_TYPE } from '~/vue_shared/components/filtered_search_bar/constants';
jest.mock('~/packages_and_registries/shared/utils');
@@ -92,7 +93,11 @@ describe('Package Search', () => {
expect(findRegistrySearch().props()).toMatchObject({
tokens: expect.arrayContaining([
- expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
+ expect.objectContaining({
+ token: PackageTypeToken,
+ type: TOKEN_TYPE_TYPE,
+ icon: 'package',
+ }),
]),
sortableFields: sortableFields(isGroupPage),
});
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 f36c5923532..9e9e08bc196 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -118,6 +118,13 @@ export const packageVersions = () => [
},
];
+export const linksData = {
+ _links: {
+ webPath: '/gitlab-org/package-15',
+ __typeName: 'PackageLinks',
+ },
+};
+
export const packageData = (extend) => ({
__typename: 'Package',
id: 'gid://gitlab/Packages::Package/111',
@@ -232,6 +239,7 @@ export const packageDetailsQuery = (extendPackage) => ({
__typename: 'PackageFileConnection',
},
versions: {
+ count: packageVersions().length,
nodes: packageVersions(),
pageInfo: {
hasNextPage: true,
@@ -376,6 +384,7 @@ export const packagesListQuery = ({ type = 'group', extend = {}, extendPaginatio
nodes: [
{
...packageData(),
+ ...linksData,
project: packageProject(),
tags: { nodes: packageTags() },
pipelines: {
@@ -387,6 +396,7 @@ export const packagesListQuery = ({ type = 'group', extend = {}, extendPaginatio
project: packageProject(),
tags: { nodes: [] },
pipelines: { nodes: [] },
+ ...linksData,
},
],
pageInfo: pagination(extendPagination),
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index f942a334f40..eb3b999c1ca 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState, GlBadge, GlTabs, GlTab, GlSprintf } from '@gitlab/ui';
+import { GlEmptyState, GlTabs, GlTab, GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -42,6 +42,7 @@ import {
packageFiles,
packageDestroyFilesMutation,
packageDestroyFilesMutationError,
+ pagination,
} from '../mock_data';
jest.mock('~/flash');
@@ -122,7 +123,9 @@ describe('PackagesApp', () => {
const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal');
const findVersionsList = () => wrapper.findComponent(PackageVersionsList);
- const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
+ const findVersionsCountBadge = () => wrapper.findByTestId('other-versions-badge');
+ const findNoVersionsMessage = () => wrapper.findByTestId('no-versions-message');
+ const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge');
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
const findDeletePackage = () => wrapper.findComponent(DeletePackage);
@@ -564,6 +567,30 @@ describe('PackagesApp', () => {
await waitForPromises();
expect(findVersionsList()).toBeDefined();
+ expect(findVersionsCountBadge().exists()).toBe(true);
+ expect(findVersionsCountBadge().text()).toBe(packageVersions().length.toString());
+ });
+
+ it('displays tab with 0 count when package has no other versions', async () => {
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(
+ packageDetailsQuery({
+ versions: {
+ count: 0,
+ nodes: [],
+ pageInfo: pagination({ hasNextPage: false, hasPreviousPage: false }),
+ },
+ }),
+ ),
+ });
+
+ await waitForPromises();
+
+ expect(findVersionsCountBadge().exists()).toBe(true);
+ expect(findVersionsCountBadge().text()).toBe('0');
+ expect(findNoVersionsMessage().text()).toMatchInterpolatedText(
+ 'There are no other versions of this package.',
+ );
});
it('binds the correct props', async () => {
@@ -576,6 +603,7 @@ describe('PackagesApp', () => {
});
});
});
+
describe('dependency links', () => {
it('does not show the dependency links for a non nuget package', async () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
index 8e08864bdb8..cbb5aa52694 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
@@ -232,6 +232,7 @@ describe('Container Expiration Policy Settings Form', () => {
describe('form', () => {
describe('form submit event', () => {
useMockLocationHelper();
+ const originalHref = window.location.href;
it('save has type submit', () => {
mountComponent();
@@ -319,7 +320,7 @@ describe('Container Expiration Policy Settings Form', () => {
await submitForm();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE);
- expect(window.location.href).toBeUndefined();
+ expect(window.location.href).toBe(originalHref);
});
it('parses the error messages', async () => {
diff --git a/spec/frontend/packages_and_registries/shared/utils_spec.js b/spec/frontend/packages_and_registries/shared/utils_spec.js
index 962cb2257ce..d81cdbfd8bd 100644
--- a/spec/frontend/packages_and_registries/shared/utils_spec.js
+++ b/spec/frontend/packages_and_registries/shared/utils_spec.js
@@ -1,4 +1,3 @@
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import {
getQueryParams,
keyValueToFilterToken,
@@ -7,6 +6,7 @@ import {
beautifyPath,
getCommitLink,
} from '~/packages_and_registries/shared/utils';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import { packageList } from 'jest/packages_and_registries/infrastructure_registry/components/mock_data';
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index 03aed7454e3..825aef27327 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -4,7 +4,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
-import { visitUrl } from '~/lib/utils/url_utility';
import Todos from '~/pages/dashboard/todos/index/todos';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -15,12 +14,10 @@ const TEST_COUNT_BIG = 2000;
const TEST_DONE_COUNT_BIG = 7300;
describe('Todos', () => {
- let todoItem;
let mock;
beforeEach(() => {
loadHTMLFixture('todos/todos.html');
- todoItem = document.querySelector('.todos-list .todo');
mock = new MockAdapter(axios);
return new Todos();
@@ -34,95 +31,47 @@ describe('Todos', () => {
mock.restore();
});
- describe('goToTodoUrl', () => {
- it('opens the todo url', () => {
- const todoLink = todoItem.dataset.url;
+ describe('on done todo click', () => {
+ let onToggleSpy;
- let expectedUrl = null;
- visitUrl.mockImplementation((url) => {
- expectedUrl = url;
- });
+ beforeEach(() => {
+ const el = document.querySelector('.js-done-todo');
+ const path = el.dataset.href;
- todoItem.click();
+ // Arrange
+ mock
+ .onDelete(path)
+ .replyOnce(200, { count: TEST_COUNT_BIG, done_count: TEST_DONE_COUNT_BIG });
+ onToggleSpy = jest.fn();
+ document.addEventListener('todo:toggle', onToggleSpy);
- expect(expectedUrl).toEqual(todoLink);
- });
-
- describe('meta click', () => {
- let windowOpenSpy;
- let metakeyEvent;
-
- beforeEach(() => {
- metakeyEvent = new MouseEvent('click', { ctrlKey: true });
- windowOpenSpy = jest.spyOn(window, 'open').mockImplementation(() => {});
- });
-
- it('opens the todo url in another tab', () => {
- const todoLink = todoItem.dataset.url;
-
- document.querySelectorAll('.todos-list .todo').forEach((el) => {
- el.dispatchEvent(metakeyEvent);
- });
-
- expect(visitUrl).not.toHaveBeenCalled();
- expect(windowOpenSpy).toHaveBeenCalledWith(todoLink, '_blank');
- });
-
- it('run native funcionality when avatar is clicked', () => {
- document.querySelectorAll('.todos-list a').forEach((el) => {
- el.addEventListener('click', (e) => e.preventDefault());
- });
- document.querySelectorAll('.todos-list img').forEach((el) => {
- el.dispatchEvent(metakeyEvent);
- });
+ // Act
+ el.click();
- expect(visitUrl).not.toHaveBeenCalled();
- expect(windowOpenSpy).not.toHaveBeenCalled();
- });
+ // Wait for axios and HTML to udpate
+ return waitForPromises();
});
- describe('on done todo click', () => {
- let onToggleSpy;
-
- beforeEach(() => {
- const el = document.querySelector('.js-done-todo');
- const path = el.dataset.href;
-
- // Arrange
- mock
- .onDelete(path)
- .replyOnce(200, { count: TEST_COUNT_BIG, done_count: TEST_DONE_COUNT_BIG });
- onToggleSpy = jest.fn();
- document.addEventListener('todo:toggle', onToggleSpy);
-
- // Act
- el.click();
-
- // Wait for axios and HTML to udpate
- return waitForPromises();
- });
-
- it('dispatches todo:toggle', () => {
- expect(onToggleSpy).toHaveBeenCalledWith(
- expect.objectContaining({
- detail: {
- count: TEST_COUNT_BIG,
- },
- }),
- );
- });
+ it('dispatches todo:toggle', () => {
+ expect(onToggleSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ detail: {
+ count: TEST_COUNT_BIG,
+ },
+ }),
+ );
+ });
- it('updates pending text', () => {
- expect(document.querySelector('.js-todos-pending .js-todos-badge').innerHTML).toEqual(
- addDelimiter(TEST_COUNT_BIG),
- );
- });
+ it('updates pending text', () => {
+ expect(document.querySelector('.js-todos-pending .js-todos-badge').innerHTML).toEqual(
+ addDelimiter(TEST_COUNT_BIG),
+ );
+ });
- it('updates done text', () => {
- expect(document.querySelector('.js-todos-done .js-todos-badge').innerHTML).toEqual(
- addDelimiter(TEST_DONE_COUNT_BIG),
- );
- });
+ it('updates done text', () => {
+ expect(document.querySelector('.js-todos-done .js-todos-badge').innerHTML).toEqual(
+ addDelimiter(TEST_DONE_COUNT_BIG),
+ );
});
});
});
diff --git a/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js b/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js
index c1e1545944b..d60730e630b 100644
--- a/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js
+++ b/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js
@@ -1,6 +1,6 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import searchUsersQuery from '~/graphql_shared/queries/users_search_all.query.graphql';
@@ -59,7 +59,7 @@ describe('fogbugz user select component', () => {
const id = 8;
- wrapper.findComponent(GlListbox).vm.$emit('select', `gid://gitlab/User/${id}`);
+ wrapper.findComponent(GlCollapsibleListbox).vm.$emit('select', `gid://gitlab/User/${id}`);
await nextTick();
expect(wrapper.get('input').attributes('value')).toBe(id.toString());
@@ -69,7 +69,7 @@ describe('fogbugz user select component', () => {
createComponent();
jest.runOnlyPendingTimers();
- wrapper.findComponent(GlListbox).vm.$emit('search', 'test');
+ wrapper.findComponent(GlCollapsibleListbox).vm.$emit('search', 'test');
await nextTick();
jest.runOnlyPendingTimers();
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 727c5164cdc..9718d847ed5 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,5 +1,5 @@
import { GlFormInputGroup, GlFormInput, GlForm, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
-import { getByRole, getAllByRole } from '@testing-library/dom';
+import { getByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -133,10 +133,15 @@ describe('ForkForm component', () => {
expect(cancelButton.attributes('href')).toBe(projectFullPath);
});
- const selectedMockNamespace = { name: 'two', full_name: 'two-group/two', id: 2 };
+ const selectedMockNamespace = {
+ name: 'two',
+ full_name: 'two-group/two',
+ id: 2,
+ visibility: 'public',
+ };
- const fillForm = () => {
- findForkUrlInput().vm.$emit('select', selectedMockNamespace);
+ const fillForm = (namespace = selectedMockNamespace) => {
+ findForkUrlInput().vm.$emit('select', namespace);
};
it('has input with csrf token', () => {
@@ -226,66 +231,139 @@ describe('ForkForm component', () => {
},
];
- it('resets the visibility to default "private"', async () => {
+ it('resets the visibility to max allowed below current level', async () => {
+ createFullComponent({ projectVisibility: 'public' }, { namespaces });
+
+ expect(wrapper.vm.form.fields.visibility.value).toBe('public');
+
+ fillForm({
+ name: 'one',
+ id: 1,
+ visibility: 'internal',
+ });
+ await nextTick();
+
+ expect(getByRole(wrapper.element, 'radio', { name: /internal/i }).checked).toBe(true);
+ });
+
+ it('does not reset the visibility when current level is allowed', async () => {
+ createFullComponent({ projectVisibility: 'public' }, { namespaces });
+
+ expect(wrapper.vm.form.fields.visibility.value).toBe('public');
+
+ fillForm({
+ name: 'two',
+ id: 2,
+ visibility: 'public',
+ });
+ await nextTick();
+
+ expect(getByRole(wrapper.element, 'radio', { name: /public/i }).checked).toBe(true);
+ });
+
+ it('does not reset the visibility when visibility cap is increased', async () => {
createFullComponent({ projectVisibility: 'public' }, { namespaces });
expect(wrapper.vm.form.fields.visibility.value).toBe('public');
- fillForm();
+ fillForm({
+ name: 'three',
+ id: 3,
+ visibility: 'internal',
+ });
+ await nextTick();
+
+ fillForm({
+ name: 'four',
+ id: 4,
+ visibility: 'public',
+ });
+ await nextTick();
+
+ expect(getByRole(wrapper.element, 'radio', { name: /internal/i }).checked).toBe(true);
+ });
+
+ it('sets the visibility to be next highest from current when restrictedVisibilityLevels is set', async () => {
+ createFullComponent(
+ { projectVisibility: 'public', restrictedVisibilityLevels: [10] },
+ { namespaces },
+ );
+
+ wrapper.vm.form.fields.visibility.value = 'internal';
+ fillForm({
+ name: 'five',
+ id: 5,
+ visibility: 'public',
+ });
await nextTick();
expect(getByRole(wrapper.element, 'radio', { name: /private/i }).checked).toBe(true);
});
- it('sets the visibility to be null when restrictedVisibilityLevels is set', async () => {
- createFullComponent({ restrictedVisibilityLevels: [10] }, { namespaces });
+ it('sets the visibility to be next lowest from current when nothing lower is allowed', async () => {
+ createFullComponent(
+ { projectVisibility: 'public', restrictedVisibilityLevels: [0] },
+ { namespaces },
+ );
+
+ fillForm({
+ name: 'six',
+ id: 6,
+ visibility: 'private',
+ });
+ await nextTick();
+
+ expect(getByRole(wrapper.element, 'radio', { name: /private/i }).checked).toBe(true);
- fillForm();
+ fillForm({
+ name: 'six',
+ id: 6,
+ visibility: 'public',
+ });
await nextTick();
- const container = getByRole(wrapper.element, 'radiogroup', { name: /visibility/i });
- const visibilityRadios = getAllByRole(container, 'radio');
- expect(visibilityRadios.filter((e) => e.checked)).toHaveLength(0);
+ expect(getByRole(wrapper.element, 'radio', { name: /internal/i }).checked).toBe(true);
});
});
it.each`
- project | restrictedVisibilityLevels
- ${'private'} | ${[]}
- ${'internal'} | ${[]}
- ${'public'} | ${[]}
- ${'private'} | ${[0]}
- ${'private'} | ${[10]}
- ${'private'} | ${[20]}
- ${'private'} | ${[0, 10]}
- ${'private'} | ${[0, 20]}
- ${'private'} | ${[10, 20]}
- ${'private'} | ${[0, 10, 20]}
- ${'internal'} | ${[0]}
- ${'internal'} | ${[10]}
- ${'internal'} | ${[20]}
- ${'internal'} | ${[0, 10]}
- ${'internal'} | ${[0, 20]}
- ${'internal'} | ${[10, 20]}
- ${'internal'} | ${[0, 10, 20]}
- ${'public'} | ${[0]}
- ${'public'} | ${[10]}
- ${'public'} | ${[0, 10]}
- ${'public'} | ${[0, 20]}
- ${'public'} | ${[10, 20]}
- ${'public'} | ${[0, 10, 20]}
- `('checks the correct radio button', ({ project, restrictedVisibilityLevels }) => {
- createFullComponent({
- projectVisibility: project,
- restrictedVisibilityLevels,
- });
+ project | restrictedVisibilityLevels | computedVisibilityLevel
+ ${'private'} | ${[]} | ${'private'}
+ ${'internal'} | ${[]} | ${'internal'}
+ ${'public'} | ${[]} | ${'public'}
+ ${'private'} | ${[0]} | ${'private'}
+ ${'private'} | ${[10]} | ${'private'}
+ ${'private'} | ${[20]} | ${'private'}
+ ${'private'} | ${[0, 10]} | ${'private'}
+ ${'private'} | ${[0, 20]} | ${'private'}
+ ${'private'} | ${[10, 20]} | ${'private'}
+ ${'private'} | ${[0, 10, 20]} | ${'private'}
+ ${'internal'} | ${[0]} | ${'internal'}
+ ${'internal'} | ${[10]} | ${'private'}
+ ${'internal'} | ${[20]} | ${'internal'}
+ ${'internal'} | ${[0, 10]} | ${'private'}
+ ${'internal'} | ${[0, 20]} | ${'internal'}
+ ${'internal'} | ${[10, 20]} | ${'private'}
+ ${'internal'} | ${[0, 10, 20]} | ${'private'}
+ ${'public'} | ${[0]} | ${'public'}
+ ${'public'} | ${[10]} | ${'public'}
+ ${'public'} | ${[0, 10]} | ${'public'}
+ ${'public'} | ${[0, 20]} | ${'internal'}
+ ${'public'} | ${[10, 20]} | ${'private'}
+ ${'public'} | ${[0, 10, 20]} | ${'private'}
+ `(
+ 'checks the correct radio button',
+ ({ project, restrictedVisibilityLevels, computedVisibilityLevel }) => {
+ createFullComponent({
+ projectVisibility: project,
+ restrictedVisibilityLevels,
+ });
- if (restrictedVisibilityLevels.length === 0) {
- expect(wrapper.find('[name="visibility"]:checked').attributes('value')).toBe(project);
- } else {
- expect(wrapper.find('[name="visibility"]:checked').exists()).toBe(false);
- }
- });
+ expect(wrapper.find('[name="visibility"]:checked').attributes('value')).toBe(
+ computedVisibilityLevel,
+ );
+ },
+ );
it.each`
project | namespace | privateIsDisabled | internalIsDisabled | publicIsDisabled | restrictedVisibilityLevels
diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
index 21a38f066d9..e7c7ec0d336 100644
--- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
+++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
@@ -36,62 +36,48 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
<!---->
- <gl-dropdown-stub
+ <gl-base-dropdown-stub
+ ariahaspopup="listbox"
category="primary"
- clearalltext="Clear all"
- clearalltextclass="gl-px-5"
- headertext=""
- hideheaderborder="true"
- highlighteditemstitle="Selected"
- highlighteditemstitleclass="gl-px-5"
+ icon=""
size="medium"
- text="rspec"
+ toggleid="dropdown-toggle-btn-6"
+ toggletext="rspec"
variant="default"
>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischecked="true"
- ischeckitem="true"
- secondarytext=""
- value="rspec"
- >
-
- rspec
-
- </gl-dropdown-item-stub>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckitem="true"
- secondarytext=""
- value="cypress"
+ <!---->
+
+ <!---->
+
+ <ul
+ aria-labelledby="dropdown-toggle-btn-6"
+ class="gl-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0"
+ id="listbox"
+ role="listbox"
+ tabindex="-1"
>
-
- cypress
-
- </gl-dropdown-item-stub>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckitem="true"
- secondarytext=""
- value="karma"
- >
-
- karma
-
- </gl-dropdown-item-stub>
- </gl-dropdown-stub>
+ <gl-listbox-item-stub
+ isselected="true"
+ >
+
+ rspec
+
+ </gl-listbox-item-stub>
+ <gl-listbox-item-stub>
+
+ cypress
+
+ </gl-listbox-item-stub>
+ <gl-listbox-item-stub>
+
+ karma
+
+ </gl-listbox-item-stub>
+ </ul>
+
+ <!---->
+
+ </gl-base-dropdown-stub>
</div>
<gl-area-chart-stub
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 2f2edd6b025..e99734963e3 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlAlert, GlListbox, GlListboxItem } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
@@ -22,9 +22,10 @@ describe('Code Coverage', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findAreaChart = () => wrapper.findComponent(GlAreaChart);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findFirstDropdownItem = () => findAllDropdownItems().at(0);
- const findSecondDropdownItem = () => findAllDropdownItems().at(1);
+ const findListBox = () => wrapper.findComponent(GlListbox);
+ const findListBoxItems = () => wrapper.findAllComponents(GlListboxItem);
+ const findFirstListBoxItem = () => findListBoxItems().at(0);
+ const findSecondListBoxItem = () => findListBoxItems().at(1);
const findDownloadButton = () => wrapper.find('[data-testid="download-button"]');
const createComponent = () => {
@@ -36,6 +37,7 @@ describe('Code Coverage', () => {
graphRef,
graphCsvPath,
},
+ stubs: { GlListbox },
});
};
@@ -142,9 +144,9 @@ describe('Code Coverage', () => {
});
it('renders the dropdown with all custom names as options', () => {
- expect(wrapper.findComponent(GlDropdown).exists()).toBeDefined();
- expect(findAllDropdownItems()).toHaveLength(codeCoverageMockData.length);
- expect(findFirstDropdownItem().text()).toBe(codeCoverageMockData[0].group_name);
+ expect(findListBox().exists()).toBe(true);
+ expect(findListBoxItems()).toHaveLength(codeCoverageMockData.length);
+ expect(findFirstListBoxItem().text()).toBe(codeCoverageMockData[0].group_name);
});
});
@@ -159,19 +161,19 @@ describe('Code Coverage', () => {
});
it('updates the selected dropdown option with an icon', async () => {
- findSecondDropdownItem().vm.$emit('click');
+ findListBox().vm.$emit('select', '1');
await nextTick();
- expect(findFirstDropdownItem().attributes('ischecked')).toBe(undefined);
- expect(findSecondDropdownItem().attributes('ischecked')).toBe('true');
+ expect(findFirstListBoxItem().attributes('isselected')).toBeUndefined();
+ expect(findSecondListBoxItem().attributes('isselected')).toBe('true');
});
it('updates the graph data when selecting a different option in dropdown', async () => {
const originalSelectedData = wrapper.vm.selectedDailyCoverage;
const expectedData = codeCoverageMockData[1];
- findSecondDropdownItem().vm.$emit('click');
+ findListBox().vm.$emit('select', '1');
await nextTick();
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
deleted file mode 100644
index 4cac642bb50..00000000000
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import { formatUtcOffset, formatTimezone } from '~/lib/utils/datetime_utility';
-import { findTimezoneByIdentifier } from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
-
-describe('Timezone Dropdown', () => {
- describe('formatUtcOffset', () => {
- it('will convert negative utc offsets in seconds to hours and minutes', () => {
- expect(formatUtcOffset(-21600)).toEqual('- 6');
- });
-
- it('will convert positive utc offsets in seconds to hours and minutes', () => {
- expect(formatUtcOffset(25200)).toEqual('+ 7');
- expect(formatUtcOffset(49500)).toEqual('+ 13.75');
- });
-
- it('will return 0 when given a string', () => {
- expect(formatUtcOffset('BLAH')).toEqual('0');
- expect(formatUtcOffset('$%$%')).toEqual('0');
- });
-
- it('will return 0 when given an array', () => {
- expect(formatUtcOffset(['an', 'array'])).toEqual('0');
- });
-
- it('will return 0 when given an object', () => {
- expect(formatUtcOffset({ some: '', object: '' })).toEqual('0');
- });
-
- it('will return 0 when given null', () => {
- expect(formatUtcOffset(null)).toEqual('0');
- });
-
- it('will return 0 when given undefined', () => {
- expect(formatUtcOffset(undefined)).toEqual('0');
- });
-
- it('will return 0 when given empty input', () => {
- expect(formatUtcOffset('')).toEqual('0');
- });
- });
-
- describe('formatTimezone', () => {
- it('given name: "Chatham Is.", offset: "49500", will format for display as "[UTC + 13.75] Chatham Is."', () => {
- expect(
- formatTimezone({
- name: 'Chatham Is.',
- offset: 49500,
- identifier: 'Pacific/Chatham',
- }),
- ).toEqual('[UTC + 13.75] Chatham Is.');
- });
-
- it('given name: "Saskatchewan", offset: "-21600", will format for display as "[UTC - 6] Saskatchewan"', () => {
- expect(
- formatTimezone({
- name: 'Saskatchewan',
- offset: -21600,
- identifier: 'America/Regina',
- }),
- ).toEqual('[UTC - 6] Saskatchewan');
- });
-
- it('given name: "Accra", offset: "0", will format for display as "[UTC 0] Accra"', () => {
- expect(
- formatTimezone({
- name: 'Accra',
- offset: 0,
- identifier: 'Africa/Accra',
- }),
- ).toEqual('[UTC 0] Accra');
- });
- });
-
- describe('findTimezoneByIdentifier', () => {
- const tzList = [
- {
- identifier: 'Asia/Tokyo',
- name: 'Sapporo',
- offset: 32400,
- },
- {
- identifier: 'Asia/Hong_Kong',
- name: 'Hong Kong',
- offset: 28800,
- },
- {
- identifier: 'Asia/Dhaka',
- name: 'Dhaka',
- offset: 21600,
- },
- ];
-
- const identifier = 'Asia/Dhaka';
- it('returns the correct object if the identifier exists', () => {
- const res = findTimezoneByIdentifier(tzList, identifier);
-
- expect(res).toBe(tzList[2]);
- });
-
- it('returns null if it doesnt find the identifier', () => {
- const res = findTimezoneByIdentifier(tzList, 'Australia/Melbourne');
-
- expect(res).toBeNull();
- });
-
- it('returns null if there is no identifier given', () => {
- expect(findTimezoneByIdentifier(tzList)).toBeNull();
- expect(findTimezoneByIdentifier(tzList, '')).toBeNull();
- });
-
- it('returns null if there is an empty or invalid array given', () => {
- expect(findTimezoneByIdentifier([], identifier)).toBeNull();
- expect(findTimezoneByIdentifier(null, identifier)).toBeNull();
- expect(findTimezoneByIdentifier(undefined, identifier)).toBeNull();
- });
- });
-});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index b202a148306..38f7a2e919d 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -24,7 +24,6 @@ const defaultProps = {
buildsAccessLevel: 20,
wikiAccessLevel: 20,
snippetsAccessLevel: 20,
- operationsAccessLevel: 20,
metricsDashboardAccessLevel: 20,
pagesAccessLevel: 10,
analyticsAccessLevel: 20,
@@ -114,9 +113,14 @@ describe('Settings Panel', () => {
const findPackageSettings = () => wrapper.findComponent({ ref: 'package-settings' });
const findPackageAccessLevel = () =>
wrapper.find('[data-testid="package-registry-access-level"]');
- const findPackageAccessLevels = () =>
- wrapper.find('[name="project[project_feature_attributes][package_registry_access_level]"]');
const findPackagesEnabledInput = () => wrapper.find('[name="project[packages_enabled]"]');
+ const findPackageRegistryEnabledInput = () => wrapper.find('[name="package_registry_enabled"]');
+ const findPackageRegistryAccessLevelHiddenInput = () =>
+ wrapper.find(
+ 'input[name="project[project_feature_attributes][package_registry_access_level]"]',
+ );
+ const findPackageRegistryApiForEveryoneEnabledInput = () =>
+ wrapper.find('[name="package_registry_api_for_everyone_enabled"]');
const findPagesSettings = () => wrapper.findComponent({ ref: 'pages-settings' });
const findPagesAccessLevels = () =>
wrapper.find('[name="project[project_feature_attributes][pages_access_level]"]');
@@ -131,9 +135,6 @@ describe('Settings Panel', () => {
wrapper.findComponent({ ref: 'metrics-visibility-settings' });
const findMetricsVisibilityInput = () =>
findMetricsVisibilitySettings().findComponent(ProjectFeatureSetting);
- const findOperationsSettings = () => wrapper.findComponent({ ref: 'operations-settings' });
- const findOperationsVisibilityInput = () =>
- findOperationsSettings().findComponent(ProjectFeatureSetting);
const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
const findEnvironmentsSettings = () => wrapper.findComponent({ ref: 'environments-settings' });
const findFeatureFlagsSettings = () => wrapper.findComponent({ ref: 'feature-flags-settings' });
@@ -141,6 +142,8 @@ describe('Settings Panel', () => {
wrapper.findComponent({ ref: 'infrastructure-settings' });
const findReleasesSettings = () => wrapper.findComponent({ ref: 'environments-settings' });
const findMonitorSettings = () => wrapper.findComponent({ ref: 'monitor-settings' });
+ const findMonitorVisibilityInput = () =>
+ findMonitorSettings().findComponent(ProjectFeatureSetting);
afterEach(() => {
wrapper.destroy();
@@ -283,7 +286,7 @@ describe('Settings Panel', () => {
});
expect(findRepositoryFeatureProjectRow().props('helpText')).toBe(
- 'View and edit files in this project. Non-project members have only read access.',
+ 'View and edit files in this project. When set to **Everyone With Access** non-project members have only read access.',
);
});
});
@@ -587,28 +590,63 @@ describe('Settings Panel', () => {
expect(findPackageAccessLevel().exists()).toBe(true);
});
+ it('has hidden input field for package registry access level', () => {
+ wrapper = mountComponent({
+ glFeatures: { packageRegistryAccessLevel: true },
+ packagesAvailable: true,
+ });
+
+ expect(findPackageRegistryAccessLevelHiddenInput().exists()).toBe(true);
+ });
+
it.each`
- visibilityLevel | output
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${[[featureAccessLevel.PROJECT_MEMBERS, 'Only Project Members'], [30, 'Everyone']]}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${[[featureAccessLevel.EVERYONE, 'Everyone With Access'], [30, 'Everyone']]}
- ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${[[30, 'Everyone']]}
+ projectVisibilityLevel | packageRegistryEnabled | packageRegistryApiForEveryoneEnabled | expectedAccessLevel
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${false} | ${'disabled'} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${true} | ${false} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${true} | ${true} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${false} | ${'disabled'} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${true} | ${false} | ${featureAccessLevel.EVERYONE}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${true} | ${true} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${false} | ${'hidden'} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${true} | ${'hidden'} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
`(
- 'renders correct options when visibilityLevel is $visibilityLevel',
- async ({ visibilityLevel, output }) => {
+ 'sets correct access level',
+ async ({
+ projectVisibilityLevel,
+ packageRegistryEnabled,
+ packageRegistryApiForEveryoneEnabled,
+ expectedAccessLevel,
+ }) => {
wrapper = mountComponent({
glFeatures: { packageRegistryAccessLevel: true },
packagesAvailable: true,
currentSettings: {
- visibilityLevel,
+ visibilityLevel: projectVisibilityLevel,
},
});
- expect(findPackageAccessLevels().props('options')).toStrictEqual(output);
+ await findPackageRegistryEnabledInput().vm.$emit('change', packageRegistryEnabled);
+
+ const packageRegistryApiForEveryoneEnabledInput = findPackageRegistryApiForEveryoneEnabledInput();
+
+ if (packageRegistryApiForEveryoneEnabled === 'hidden') {
+ expect(packageRegistryApiForEveryoneEnabledInput.exists()).toBe(false);
+ } else if (packageRegistryApiForEveryoneEnabled === 'disabled') {
+ expect(packageRegistryApiForEveryoneEnabledInput.props('disabled')).toBe(true);
+ } else {
+ expect(packageRegistryApiForEveryoneEnabledInput.props('disabled')).toBe(false);
+ await packageRegistryApiForEveryoneEnabledInput.vm.$emit(
+ 'change',
+ packageRegistryApiForEveryoneEnabled,
+ );
+ }
+
+ expect(wrapper.vm.packageRegistryAccessLevel).toBe(expectedAccessLevel);
},
);
it.each`
- initialProjectVisibilityLevel | newProjectVisibilityLevel | initialPackageRegistryOption | expectedPackageRegistryOption
+ initialProjectVisibilityLevel | newProjectVisibilityLevel | initialAccessLevel | expectedAccessLevel
${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.EVERYONE}
${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
@@ -626,27 +664,25 @@ describe('Settings Panel', () => {
${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.EVERYONE}
`(
- 'changes option from $initialPackageRegistryOption to $expectedPackageRegistryOption when visibilityLevel changed from $initialProjectVisibilityLevel to $newProjectVisibilityLevel',
+ 'changes access level when project visibility level changed',
async ({
initialProjectVisibilityLevel,
newProjectVisibilityLevel,
- initialPackageRegistryOption,
- expectedPackageRegistryOption,
+ initialAccessLevel,
+ expectedAccessLevel,
}) => {
wrapper = mountComponent({
glFeatures: { packageRegistryAccessLevel: true },
packagesAvailable: true,
currentSettings: {
visibilityLevel: initialProjectVisibilityLevel,
- packageRegistryAccessLevel: initialPackageRegistryOption,
+ packageRegistryAccessLevel: initialAccessLevel,
},
});
await findProjectVisibilityLevelInput().setValue(newProjectVisibilityLevel);
- expect(findPackageAccessLevels().props('value')).toStrictEqual(
- expectedPackageRegistryOption,
- );
+ expect(wrapper.vm.packageRegistryAccessLevel).toBe(expectedAccessLevel);
},
);
});
@@ -751,27 +787,27 @@ describe('Settings Panel', () => {
${featureAccessLevel.EVERYONE} | ${featureAccessLevel.NOT_ENABLED}
${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.NOT_ENABLED}
`(
- 'when updating Operations Settings access level from `$before` to `$after`, Metric Dashboard access is updated to `$after` as well',
+ 'when updating Monitor access level from `$before` to `$after`, Metric Dashboard access is updated to `$after` as well',
async ({ before, after }) => {
wrapper = mountComponent({
- currentSettings: { operationsAccessLevel: before, metricsDashboardAccessLevel: before },
+ currentSettings: { monitorAccessLevel: before, metricsDashboardAccessLevel: before },
});
- await findOperationsVisibilityInput().vm.$emit('change', after);
+ await findMonitorVisibilityInput().vm.$emit('change', after);
expect(findMetricsVisibilityInput().props('value')).toBe(after);
},
);
- it('when updating Operations Settings access level from `10` to `20`, Metric Dashboard access is not increased', async () => {
+ it('when updating Monitor access level from `10` to `20`, Metric Dashboard access is not increased', async () => {
wrapper = mountComponent({
currentSettings: {
- operationsAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
+ monitorAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
},
});
- await findOperationsVisibilityInput().vm.$emit('change', featureAccessLevel.EVERYONE);
+ await findMonitorVisibilityInput().vm.$emit('change', featureAccessLevel.EVERYONE);
expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.PROJECT_MEMBERS);
});
@@ -780,7 +816,7 @@ describe('Settings Panel', () => {
wrapper = mountComponent({
currentSettings: {
visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER,
- operationsAccessLevel: featureAccessLevel.EVERYONE,
+ monitorAccessLevel: featureAccessLevel.EVERYONE,
metricsDashboardAccessLevel: featureAccessLevel.EVERYONE,
},
});
@@ -799,84 +835,32 @@ describe('Settings Panel', () => {
});
});
- describe('Operations', () => {
- it('should show the operations toggle', () => {
- wrapper = mountComponent();
-
- expect(findOperationsSettings().exists()).toBe(true);
- });
- });
-
describe('Environments', () => {
- describe('with feature flag', () => {
- it('should show the environments toggle', () => {
- wrapper = mountComponent({
- glFeatures: { splitOperationsVisibilityPermissions: true },
- });
+ it('should show the environments toggle', () => {
+ wrapper = mountComponent({});
- expect(findEnvironmentsSettings().exists()).toBe(true);
- });
- });
- describe('without feature flag', () => {
- it('should not show the environments toggle', () => {
- wrapper = mountComponent({});
-
- expect(findEnvironmentsSettings().exists()).toBe(false);
- });
+ expect(findEnvironmentsSettings().exists()).toBe(true);
});
});
describe('Feature Flags', () => {
- describe('with feature flag', () => {
- it('should show the feature flags toggle', () => {
- wrapper = mountComponent({
- glFeatures: { splitOperationsVisibilityPermissions: true },
- });
-
- expect(findFeatureFlagsSettings().exists()).toBe(true);
- });
- });
- describe('without feature flag', () => {
- it('should not show the feature flags toggle', () => {
- wrapper = mountComponent({});
+ it('should show the feature flags toggle', () => {
+ wrapper = mountComponent({});
- expect(findFeatureFlagsSettings().exists()).toBe(false);
- });
+ expect(findFeatureFlagsSettings().exists()).toBe(true);
});
});
describe('Infrastructure', () => {
- describe('with feature flag', () => {
- it('should show the infrastructure toggle', () => {
- wrapper = mountComponent({
- glFeatures: { splitOperationsVisibilityPermissions: true },
- });
+ it('should show the infrastructure toggle', () => {
+ wrapper = mountComponent({});
- expect(findInfrastructureSettings().exists()).toBe(true);
- });
- });
- describe('without feature flag', () => {
- it('should not show the infrastructure toggle', () => {
- wrapper = mountComponent({});
-
- expect(findInfrastructureSettings().exists()).toBe(false);
- });
+ expect(findInfrastructureSettings().exists()).toBe(true);
});
});
describe('Releases', () => {
- describe('with feature flag', () => {
- it('should show the releases toggle', () => {
- wrapper = mountComponent({
- glFeatures: { splitOperationsVisibilityPermissions: true },
- });
+ it('should show the releases toggle', () => {
+ wrapper = mountComponent({});
- expect(findReleasesSettings().exists()).toBe(true);
- });
- });
- describe('without feature flag', () => {
- it('should not show the releases toggle', () => {
- wrapper = mountComponent({});
-
- expect(findReleasesSettings().exists()).toBe(false);
- });
+ expect(findReleasesSettings().exists()).toBe(true);
});
});
describe('Monitor', () => {
@@ -884,37 +868,20 @@ describe('Settings Panel', () => {
[10, 'Only Project Members'],
[20, 'Everyone With Access'],
];
- describe('with feature flag', () => {
- it('shows Monitor toggle instead of Operations toggle', () => {
- wrapper = mountComponent({
- glFeatures: { splitOperationsVisibilityPermissions: true },
- });
-
- expect(findMonitorSettings().exists()).toBe(true);
- expect(findOperationsSettings().exists()).toBe(false);
- expect(findMonitorSettings().findComponent(ProjectFeatureSetting).props('options')).toEqual(
- expectedAccessLevel,
- );
- });
- it('when monitorAccessLevel is for project members, it is also for everyone', () => {
- wrapper = mountComponent({
- glFeatures: { splitOperationsVisibilityPermissions: true },
- currentSettings: { monitorAccessLevel: featureAccessLevel.PROJECT_MEMBERS },
- });
+ it('shows Monitor toggle instead of Operations toggle', () => {
+ wrapper = mountComponent({});
- expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.EVERYONE);
- });
+ expect(findMonitorSettings().exists()).toBe(true);
+ expect(findMonitorSettings().findComponent(ProjectFeatureSetting).props('options')).toEqual(
+ expectedAccessLevel,
+ );
});
- describe('without feature flag', () => {
- it('shows Operations toggle instead of Monitor toggle', () => {
- wrapper = mountComponent({});
-
- expect(findMonitorSettings().exists()).toBe(false);
- expect(findOperationsSettings().exists()).toBe(true);
- expect(
- findOperationsSettings().findComponent(ProjectFeatureSetting).props('options'),
- ).toEqual(expectedAccessLevel);
+ it('when monitorAccessLevel is for project members, it is also for everyone', () => {
+ wrapper = mountComponent({
+ currentSettings: { monitorAccessLevel: featureAccessLevel.PROJECT_MEMBERS },
});
+
+ expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.EVERYONE);
});
});
});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
index 982c81b9272..7c9aae13d25 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
@@ -3,13 +3,13 @@ import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import WikiContent from '~/pages/shared/wikis/components/wiki_content.vue';
-import { renderGFM } from '~/pages/shared/wikis/render_gfm_facade';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import { handleLocationHash } from '~/lib/utils/common_utils';
-jest.mock('~/pages/shared/wikis/render_gfm_facade');
+jest.mock('~/behaviors/markdown/render_gfm');
jest.mock('~/lib/utils/common_utils');
describe('pages/shared/wikis/components/wiki_content', () => {
diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js
index 437d51e02ba..5ab2c9abe5d 100644
--- a/spec/frontend/performance_bar/components/detailed_metric_spec.js
+++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js
@@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdownItem } from '@gitlab/ui';
import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -31,12 +30,8 @@ describe('detailedMetric', () => {
const findExpandedBacktraceBtnAtIndex = (index) => findExpandBacktraceBtns().at(index);
const findDetailsLabel = () => wrapper.findByTestId('performance-bar-details-label');
const findSortOrderDropdown = () => wrapper.findByTestId('performance-bar-sort-order');
- const clickSortOrderDropdownItem = (sortOrder) =>
- findSortOrderDropdown()
- .findAllComponents(GlDropdownItem)
- .filter((item) => item.text() === sortOrderOptions[sortOrder])
- .at(0)
- .vm.$emit('click');
+ const selectSortOrder = (sortOrder) =>
+ findSortOrderDropdown().vm.$emit('select', sortOrderOptions[sortOrder].value);
const findEmptyDetailNotice = () => wrapper.findByTestId('performance-bar-empty-detail-notice');
const findAllDetailDurations = () =>
wrapper.findAllByTestId('performance-item-duration').wrappers.map((w) => w.text());
@@ -334,11 +329,11 @@ describe('detailedMetric', () => {
});
it('changes sortOrder on select', async () => {
- clickSortOrderDropdownItem(sortOrders.CHRONOLOGICAL);
+ selectSortOrder(sortOrders.CHRONOLOGICAL);
await nextTick();
expect(findAllDetailDurations()).toEqual(['23ms', '100ms', '75ms']);
- clickSortOrderDropdownItem(sortOrders.DURATION);
+ selectSortOrder(sortOrders.DURATION);
await nextTick();
expect(findAllDetailDurations()).toEqual(['100ms', '75ms', '23ms']);
});
diff --git a/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js
deleted file mode 100644
index 512b152f106..00000000000
--- a/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js
+++ /dev/null
@@ -1,456 +0,0 @@
-import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue';
-import { TEST_HOST } from 'helpers/test_constants';
-import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
-import { redirectTo } from '~/lib/utils/url_utility';
-import LegacyPipelineNewForm from '~/pipeline_new/components/legacy_pipeline_new_form.vue';
-import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
-import {
- mockQueryParams,
- mockPostParams,
- mockProjectId,
- mockError,
- mockRefs,
- mockCreditCardValidationRequiredError,
-} from '../mock_data';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- redirectTo: jest.fn(),
-}));
-
-const projectRefsEndpoint = '/root/project/refs';
-const pipelinesPath = '/root/project/-/pipelines';
-const configVariablesPath = '/root/project/-/pipelines/config_variables';
-const newPipelinePostResponse = { id: 1 };
-const defaultBranch = 'main';
-
-describe('Pipeline New Form', () => {
- let wrapper;
- let mock;
- let dummySubmitEvent;
-
- const findForm = () => wrapper.findComponent(GlForm);
- const findRefsDropdown = () => wrapper.findComponent(RefsDropdown);
- const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]');
- const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
- const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
- const findDropdowns = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-type"]');
- const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
- const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]');
- const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]');
- const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]');
- const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf);
- const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
- const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
-
- const selectBranch = (branch) => {
- // Select a branch in the dropdown
- findRefsDropdown().vm.$emit('input', {
- shortName: branch,
- fullName: `refs/heads/${branch}`,
- });
- };
-
- const createComponent = (props = {}, method = shallowMount) => {
- wrapper = method(LegacyPipelineNewForm, {
- provide: {
- projectRefsEndpoint,
- },
- propsData: {
- projectId: mockProjectId,
- pipelinesPath,
- configVariablesPath,
- defaultBranch,
- refParam: defaultBranch,
- settingsLink: '',
- maxWarnings: 25,
- ...props,
- },
- });
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {});
- mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs);
-
- dummySubmitEvent = {
- preventDefault: jest.fn(),
- };
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
- mock.restore();
- });
-
- describe('Form', () => {
- beforeEach(async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
-
- await waitForPromises();
- });
-
- it('displays the correct values for the provided query params', async () => {
- expect(findDropdowns().at(0).props('text')).toBe('Variable');
- expect(findDropdowns().at(1).props('text')).toBe('File');
- expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' });
- expect(findVariableRows()).toHaveLength(3);
- });
-
- it('displays a variable from provided query params', () => {
- expect(findKeyInputs().at(0).element.value).toBe('test_var');
- expect(findValueInputs().at(0).element.value).toBe('test_var_val');
- });
-
- it('displays an empty variable for the user to fill out', async () => {
- expect(findKeyInputs().at(2).element.value).toBe('');
- expect(findValueInputs().at(2).element.value).toBe('');
- expect(findDropdowns().at(2).props('text')).toBe('Variable');
- });
-
- it('does not display remove icon for last row', () => {
- expect(findRemoveIcons()).toHaveLength(2);
- });
-
- it('removes ci variable row on remove icon button click', async () => {
- findRemoveIcons().at(1).trigger('click');
-
- await nextTick();
-
- expect(findVariableRows()).toHaveLength(2);
- });
-
- it('creates blank variable on input change event', async () => {
- const input = findKeyInputs().at(2);
- input.element.value = 'test_var_2';
- input.trigger('change');
-
- await nextTick();
-
- expect(findVariableRows()).toHaveLength(4);
- expect(findKeyInputs().at(3).element.value).toBe('');
- expect(findValueInputs().at(3).element.value).toBe('');
- });
- });
-
- describe('Pipeline creation', () => {
- beforeEach(async () => {
- mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
-
- await waitForPromises();
- });
-
- it('does not submit the native HTML form', async () => {
- createComponent();
-
- findForm().vm.$emit('submit', dummySubmitEvent);
-
- expect(dummySubmitEvent.preventDefault).toHaveBeenCalled();
- });
-
- it('disables the submit button immediately after submitting', async () => {
- createComponent();
-
- expect(findSubmitButton().props('disabled')).toBe(false);
-
- findForm().vm.$emit('submit', dummySubmitEvent);
- await waitForPromises();
-
- expect(findSubmitButton().props('disabled')).toBe(true);
- });
-
- it('creates pipeline with full ref and variables', async () => {
- createComponent();
-
- findForm().vm.$emit('submit', dummySubmitEvent);
- await waitForPromises();
-
- expect(getFormPostParams().ref).toEqual(`refs/heads/${defaultBranch}`);
- expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
- });
-
- it('creates a pipeline with short ref and variables from the query params', async () => {
- createComponent(mockQueryParams);
-
- await waitForPromises();
-
- findForm().vm.$emit('submit', dummySubmitEvent);
-
- await waitForPromises();
-
- expect(getFormPostParams()).toEqual(mockPostParams);
- expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
- });
- });
-
- describe('When the ref has been changed', () => {
- beforeEach(async () => {
- createComponent({}, mount);
-
- await waitForPromises();
- });
- it('variables persist between ref changes', async () => {
- selectBranch('main');
-
- await waitForPromises();
-
- const mainInput = findKeyInputs().at(0);
- mainInput.element.value = 'build_var';
- mainInput.trigger('change');
-
- await nextTick();
-
- selectBranch('branch-1');
-
- await waitForPromises();
-
- const branchOneInput = findKeyInputs().at(0);
- branchOneInput.element.value = 'deploy_var';
- branchOneInput.trigger('change');
-
- await nextTick();
-
- selectBranch('main');
-
- await waitForPromises();
-
- expect(findKeyInputs().at(0).element.value).toBe('build_var');
- expect(findVariableRows().length).toBe(2);
-
- selectBranch('branch-1');
-
- await waitForPromises();
-
- expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
- expect(findVariableRows().length).toBe(2);
- });
- });
-
- describe('when yml defines a variable', () => {
- const mockYmlKey = 'yml_var';
- const mockYmlValue = 'yml_var_val';
- const mockYmlMultiLineValue = `A value
- with multiple
- lines`;
- const mockYmlDesc = 'A var from yml.';
-
- it('loading icon is shown when content is requested and hidden when received', async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: mockYmlDesc,
- },
- });
-
- expect(findLoadingIcon().exists()).toBe(true);
-
- await waitForPromises();
-
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('multi-line strings are added to the value field without removing line breaks', async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlMultiLineValue,
- description: mockYmlDesc,
- },
- });
-
- await waitForPromises();
-
- expect(findValueInputs().at(0).element.value).toBe(mockYmlMultiLineValue);
- });
-
- describe('with description', () => {
- beforeEach(async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: mockYmlDesc,
- },
- });
-
- await waitForPromises();
- });
-
- it('displays all the variables', async () => {
- expect(findVariableRows()).toHaveLength(4);
- });
-
- it('displays a variable from yml', () => {
- expect(findKeyInputs().at(0).element.value).toBe(mockYmlKey);
- expect(findValueInputs().at(0).element.value).toBe(mockYmlValue);
- });
-
- it('displays a variable from provided query params', () => {
- expect(findKeyInputs().at(1).element.value).toBe('test_var');
- expect(findValueInputs().at(1).element.value).toBe('test_var_val');
- });
-
- it('adds a description to the first variable from yml', () => {
- expect(findVariableRows().at(0).text()).toContain(mockYmlDesc);
- });
-
- it('removes the description when a variable key changes', async () => {
- findKeyInputs().at(0).element.value = 'yml_var_modified';
- findKeyInputs().at(0).trigger('change');
-
- await nextTick();
-
- expect(findVariableRows().at(0).text()).not.toContain(mockYmlDesc);
- });
- });
-
- describe('without description', () => {
- beforeEach(async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: null,
- },
- yml_var2: {
- value: 'yml_var2_val',
- },
- yml_var3: {
- description: '',
- },
- });
-
- await waitForPromises();
- });
-
- it('displays all the variables', async () => {
- expect(findVariableRows()).toHaveLength(3);
- });
- });
- });
-
- describe('Form errors and warnings', () => {
- beforeEach(() => {
- createComponent();
- });
-
- describe('when the refs cannot be loaded', () => {
- beforeEach(() => {
- mock
- .onGet(projectRefsEndpoint, { params: { search: '' } })
- .reply(httpStatusCodes.INTERNAL_SERVER_ERROR);
-
- findRefsDropdown().vm.$emit('loadingError');
- });
-
- it('shows both an error alert', () => {
- expect(findErrorAlert().exists()).toBe(true);
- expect(findWarningAlert().exists()).toBe(false);
- });
- });
-
- describe('when the error response can be handled', () => {
- beforeEach(async () => {
- mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError);
-
- findForm().vm.$emit('submit', dummySubmitEvent);
-
- await waitForPromises();
- });
-
- it('shows both error and warning', () => {
- expect(findErrorAlert().exists()).toBe(true);
- expect(findWarningAlert().exists()).toBe(true);
- });
-
- it('shows the correct error', () => {
- expect(findErrorAlert().text()).toBe(mockError.errors[0]);
- });
-
- it('shows the correct warning title', () => {
- const { length } = mockError.warnings;
-
- expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
- });
-
- it('shows the correct amount of warnings', () => {
- expect(findWarnings()).toHaveLength(mockError.warnings.length);
- });
-
- it('re-enables the submit button', () => {
- expect(findSubmitButton().props('disabled')).toBe(false);
- });
-
- it('does not show the credit card validation required alert', () => {
- expect(findCCAlert().exists()).toBe(false);
- });
-
- describe('when the error response is credit card validation required', () => {
- beforeEach(async () => {
- mock
- .onPost(pipelinesPath)
- .reply(httpStatusCodes.BAD_REQUEST, mockCreditCardValidationRequiredError);
-
- window.gon = {
- subscriptions_url: TEST_HOST,
- payment_form_url: TEST_HOST,
- };
-
- findForm().vm.$emit('submit', dummySubmitEvent);
-
- await waitForPromises();
- });
-
- it('shows credit card validation required alert', () => {
- expect(findErrorAlert().exists()).toBe(false);
- expect(findCCAlert().exists()).toBe(true);
- });
-
- it('clears error and hides the alert on dismiss', async () => {
- expect(findCCAlert().exists()).toBe(true);
- expect(wrapper.vm.$data.error).toBe(mockCreditCardValidationRequiredError.errors[0]);
-
- findCCAlert().vm.$emit('dismiss');
-
- await nextTick();
-
- expect(findCCAlert().exists()).toBe(false);
- expect(wrapper.vm.$data.error).toBe(null);
- });
- });
- });
-
- describe('when the error response cannot be handled', () => {
- beforeEach(async () => {
- mock
- .onPost(pipelinesPath)
- .reply(httpStatusCodes.INTERNAL_SERVER_ERROR, 'something went wrong');
-
- findForm().vm.$emit('submit', dummySubmitEvent);
-
- await waitForPromises();
- });
-
- it('re-enables the submit button', () => {
- expect(findSubmitButton().props('disabled')).toBe(false);
- });
- });
- });
-});
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
index 3e699b93fd3..2360dd7d103 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -295,11 +295,11 @@ describe('Pipeline New Form', () => {
expect(dropdownItems.at(2).text()).toBe(valueOptions[2]);
});
- it('variables with multiple predefined values sets the first option as the default', () => {
+ it('variable with multiple predefined values sets value as the default', () => {
const dropdown = findValueDropdowns().at(0);
const { valueOptions } = mockYamlVariables[2];
- expect(dropdown.props('text')).toBe(valueOptions[0]);
+ expect(dropdown.props('text')).toBe(valueOptions[1]);
});
});
diff --git a/spec/frontend/pipeline_new/mock_data.js b/spec/frontend/pipeline_new/mock_data.js
index e95a65171fc..2af0ef4d7c4 100644
--- a/spec/frontend/pipeline_new/mock_data.js
+++ b/spec/frontend/pipeline_new/mock_data.js
@@ -83,7 +83,7 @@ export const mockYamlVariables = [
{
description: 'This is a variable with predefined values.',
key: 'VAR_WITH_OPTIONS',
- value: 'development',
+ value: 'staging',
valueOptions: ['development', 'staging', 'production'],
},
];
@@ -105,7 +105,7 @@ export const mockYamlVariablesWithoutDesc = [
{
description: null,
key: 'VAR_WITH_OPTIONS',
- value: 'development',
+ value: 'staging',
valueOptions: ['development', 'staging', 'production'],
},
];
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
index 7fa8a18ea1f..036b82530d5 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
@@ -48,7 +48,6 @@ describe('Pipeline Mini Graph', () => {
isMergeTrain: false,
pipelinePath: '',
stages: expect.any(Array),
- stagesClass: '',
updateDropdown: false,
upstreamPipeline: undefined,
});
@@ -63,15 +62,6 @@ describe('Pipeline Mini Graph', () => {
expect(findUpstreamArrowIcon().exists()).toBe(false);
expect(findDownstreamArrowIcon().exists()).toBe(false);
});
-
- it('triggers events in "action request complete"', () => {
- createComponent();
-
- findPipelineMiniGraph(0).vm.$emit('pipelineActionRequestComplete');
- findPipelineMiniGraph(1).vm.$emit('pipelineActionRequestComplete');
-
- expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2);
- });
});
describe('rendered state with upstream pipeline', () => {
@@ -92,7 +82,6 @@ describe('Pipeline Mini Graph', () => {
isMergeTrain: false,
pipelinePath: '',
stages: expect.any(Array),
- stagesClass: '',
updateDropdown: false,
upstreamPipeline: expect.any(Object),
});
@@ -124,7 +113,6 @@ describe('Pipeline Mini Graph', () => {
isMergeTrain: false,
pipelinePath: 'my/pipeline/path',
stages: expect.any(Array),
- stagesClass: '',
updateDropdown: false,
upstreamPipeline: undefined,
});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
index 52b440f18bb..b7a9297d856 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
@@ -186,7 +186,7 @@ describe('Pipelines stage component', () => {
});
});
- describe('pipelineActionRequestComplete', () => {
+ describe('job update in dropdown', () => {
beforeEach(async () => {
mock.onGet(dropdownPath).reply(200, stageReply);
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
@@ -204,24 +204,11 @@ describe('Pipelines stage component', () => {
await findCiActionBtn().trigger('click');
};
- it('closes dropdown when job item action is clicked', async () => {
- const hidden = jest.fn();
-
- wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
-
- expect(hidden).toHaveBeenCalledTimes(0);
-
- await clickCiAction();
- await waitForPromises();
-
- expect(hidden).toHaveBeenCalledTimes(1);
- });
-
- it('emits `pipelineActionRequestComplete` when job item action is clicked', async () => {
+ it('keeps dropdown open when job item action is clicked', async () => {
await clickCiAction();
await waitForPromises();
- expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(1);
+ expect(findDropdown().classes('show')).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
index bfb780d5d39..c123f53886e 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
@@ -26,12 +26,6 @@ describe('Pipeline Stages', () => {
expect(findPipelineStages()).toHaveLength(mockStages.length);
});
- it('renders stages with a custom class', () => {
- createComponent({ stagesClass: 'my-class' });
-
- expect(wrapper.findAll('.my-class')).toHaveLength(mockStages.length);
- });
-
it('does not fail when stages are empty', () => {
createComponent({ stages: [] });
@@ -39,15 +33,6 @@ describe('Pipeline Stages', () => {
expect(findPipelineStages()).toHaveLength(0);
});
- it('triggers events in "action request complete" in stages', () => {
- createComponent();
-
- findPipelineStagesAt(0).vm.$emit('pipelineActionRequestComplete');
- findPipelineStagesAt(1).vm.$emit('pipelineActionRequestComplete');
-
- expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2);
- });
-
it('update dropdown is false by default', () => {
createComponent();
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index ee3eaaf5ef3..ba7262353f0 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -6,7 +6,10 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ FILTERED_SEARCH_TERM,
+ OPERATORS_IS,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
import { users, mockSearch, branches, tags } from '../mock_data';
@@ -63,7 +66,7 @@ describe('Pipelines filtered search', () => {
title: 'Trigger author',
unique: true,
projectId: '21',
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
});
expect(findBranchToken()).toMatchObject({
@@ -73,7 +76,7 @@ describe('Pipelines filtered search', () => {
unique: true,
projectId: '21',
defaultBranchName: 'main',
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
});
expect(findSourceToken()).toMatchObject({
@@ -81,7 +84,7 @@ describe('Pipelines filtered search', () => {
icon: 'trigger-source',
title: 'Source',
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
});
expect(findStatusToken()).toMatchObject({
@@ -89,7 +92,7 @@ describe('Pipelines filtered search', () => {
icon: 'status',
title: 'Status',
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
});
expect(findTagToken()).toMatchObject({
@@ -97,7 +100,7 @@ describe('Pipelines filtered search', () => {
icon: 'tag',
title: 'Tag name',
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
});
});
@@ -111,7 +114,7 @@ describe('Pipelines filtered search', () => {
it('disables tag name token when branch name token is active', async () => {
findFilteredSearch().vm.$emit('input', [
{ type: 'ref', value: { data: 'branch-1', operator: '=' } },
- { type: 'filtered-search-term', value: { data: '' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: '' } },
]);
await nextTick();
@@ -122,7 +125,7 @@ describe('Pipelines filtered search', () => {
it('disables branch name token when tag name token is active', async () => {
findFilteredSearch().vm.$emit('input', [
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
- { type: 'filtered-search-term', value: { data: '' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: '' } },
]);
await nextTick();
@@ -139,7 +142,7 @@ describe('Pipelines filtered search', () => {
});
it('resets tokens disabled state when clearing tokens by backspace', async () => {
- findFilteredSearch().vm.$emit('input', [{ type: 'filtered-search-term', value: { data: '' } }]);
+ findFilteredSearch().vm.$emit('input', [{ type: FILTERED_SEARCH_TERM, value: { data: '' } }]);
await nextTick();
expect(findBranchToken().disabled).toBe(false);
@@ -172,7 +175,7 @@ describe('Pipelines filtered search', () => {
operator: '=',
},
},
- { type: 'filtered-search-term', value: { data: '' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: '' } },
];
expect(findFilteredSearch().props('value')).toMatchObject(expectedValueProp);
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
index b537c81da3f..f255e0d857f 100644
--- a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
@@ -13,7 +13,7 @@ import {
RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT,
RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT,
I18N,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
const pipelineEditorPath = '/-/ci/editor';
const ciRunnerSettingsPath = '/-/settings/ci_cd';
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
index d9199f3b0f7..df10742fd93 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -1,7 +1,7 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture } from 'helpers/fixtures';
-import { CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
+import { CI_CONFIG_STATUS_VALID } from '~/ci/pipeline_editor/constants';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 044683ce533..740037a5ac8 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -17,7 +17,6 @@ import {
TRACKING_CATEGORIES,
} from '~/pipelines/constants';
-import eventHub from '~/pipelines/event_hub';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
jest.mock('~/pipelines/event_hub');
@@ -134,12 +133,6 @@ describe('Pipelines Table', () => {
expect(findPipelineMiniGraph().props('stages')).toHaveLength(0);
});
});
-
- it('when action request is complete, should refresh table', () => {
- findPipelineMiniGraph().vm.$emit('pipelineActionRequestComplete');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
- });
});
describe('duration cell', () => {
diff --git a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
index 94f9a37f707..c090fd353f7 100644
--- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
@@ -2,6 +2,10 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitl
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import PipelineStatusToken from '~/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue';
+import {
+ TOKEN_TITLE_STATUS,
+ TOKEN_TYPE_STATUS,
+} from '~/vue_shared/components/filtered_search_bar/constants';
describe('Pipeline Status Token', () => {
let wrapper;
@@ -13,9 +17,9 @@ describe('Pipeline Status Token', () => {
const defaultProps = {
config: {
- type: 'status',
+ type: TOKEN_TYPE_STATUS,
icon: 'status',
- title: 'Status',
+ title: TOKEN_TITLE_STATUS,
unique: true,
},
value: {
diff --git a/spec/frontend/popovers/components/popovers_spec.js b/spec/frontend/popovers/components/popovers_spec.js
index eba6b95214d..1299e7277d1 100644
--- a/spec/frontend/popovers/components/popovers_spec.js
+++ b/spec/frontend/popovers/components/popovers_spec.js
@@ -57,12 +57,13 @@ describe('popovers/components/popovers.vue', () => {
describe('supports HTML content', () => {
const svgIcon = '<svg><use xlink:href="icons.svg#test"></use></svg>';
+ const escapedSvgIcon = '<svg><use xlink:href=&quot;icons.svg#test&quot;></use></svg>';
it.each`
description | content | render
${'renders html content correctly'} | ${'<b>HTML</b>'} | ${'<b>HTML</b>'}
${'removes any unsafe content'} | ${'<script>alert(XSS)</script>'} | ${''}
- ${'renders svg icons correctly'} | ${svgIcon} | ${svgIcon}
+ ${'renders svg icons correctly'} | ${svgIcon} | ${escapedSvgIcon}
`('$description', async ({ content, render }) => {
await buildWrapper(createPopoverTarget({ content, html: true }));
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index e2848e615c3..a84dd246f5d 100644
--- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
@@ -13,7 +13,7 @@ describe('BranchesDropdown', () => {
let store;
const spyFetchBranches = jest.fn();
- const createComponent = (term, state = { isFetching: false }) => {
+ const createComponent = (props, state = { isFetching: false }) => {
store = new Vuex.Store({
getters: {
joinedBranches: () => ['_main_', '_branch_1_', '_branch_2_'],
@@ -28,7 +28,8 @@ describe('BranchesDropdown', () => {
shallowMount(BranchesDropdown, {
store,
propsData: {
- value: term,
+ value: props.value,
+ blanked: props.blanked || false,
},
}),
);
@@ -48,23 +49,40 @@ describe('BranchesDropdown', () => {
describe('On mount', () => {
beforeEach(() => {
- createComponent('');
+ createComponent({ value: '' });
});
it('invokes fetchBranches', () => {
expect(spyFetchBranches).toHaveBeenCalled();
});
+
+ describe('with a value but visually blanked', () => {
+ beforeEach(() => {
+ createComponent({ value: '_main_', blanked: true }, { branch: '_main_' });
+ });
+
+ it('renders all branches', () => {
+ expect(findAllDropdownItems()).toHaveLength(3);
+ expect(findDropdownItemByIndex(0).text()).toBe('_main_');
+ expect(findDropdownItemByIndex(1).text()).toBe('_branch_1_');
+ expect(findDropdownItemByIndex(2).text()).toBe('_branch_2_');
+ });
+
+ it('selects the active branch', () => {
+ expect(wrapper.vm.isSelected('_main_')).toBe(true);
+ });
+ });
});
describe('Loading states', () => {
it('shows loading icon while fetching', () => {
- createComponent('', { isFetching: true });
+ createComponent({ value: '' }, { isFetching: true });
expect(findLoading().isVisible()).toBe(true);
});
it('does not show loading icon', () => {
- createComponent('');
+ createComponent({ value: '' });
expect(findLoading().isVisible()).toBe(false);
});
@@ -72,7 +90,7 @@ describe('BranchesDropdown', () => {
describe('No branches found', () => {
beforeEach(() => {
- createComponent('_non_existent_branch_');
+ createComponent({ value: '_non_existent_branch_' });
});
it('renders empty results message', () => {
@@ -90,7 +108,7 @@ describe('BranchesDropdown', () => {
describe('Search term is empty', () => {
beforeEach(() => {
- createComponent('');
+ createComponent({ value: '' });
});
it('renders all branches when search term is empty', () => {
@@ -107,7 +125,7 @@ describe('BranchesDropdown', () => {
describe('When searching', () => {
beforeEach(() => {
- createComponent('');
+ createComponent({ value: '' });
});
it('invokes fetchBranches', async () => {
@@ -124,7 +142,7 @@ describe('BranchesDropdown', () => {
describe('Branches found', () => {
beforeEach(() => {
- createComponent('_branch_1_', { branch: '_branch_1_' });
+ createComponent({ value: '_branch_1_' }, { branch: '_branch_1_' });
});
it('renders only the branch searched for', () => {
@@ -156,7 +174,7 @@ describe('BranchesDropdown', () => {
describe('Case insensitive for search term', () => {
beforeEach(() => {
- createComponent('_BrAnCh_1_');
+ createComponent({ value: '_BrAnCh_1_' });
});
it('renders only the branch searched for', () => {
diff --git a/spec/frontend/projects/new/components/new_project_url_select_spec.js b/spec/frontend/projects/new/components/new_project_url_select_spec.js
index b6d4ee32cf5..67532cea61e 100644
--- a/spec/frontend/projects/new/components/new_project_url_select_spec.js
+++ b/spec/frontend/projects/new/components/new_project_url_select_spec.js
@@ -63,6 +63,8 @@ describe('NewProjectUrlSelect component', () => {
rootUrl: 'https://gitlab.com/',
trackLabel: 'blank_project',
userNamespaceId: '1',
+ inputId: 'input_id',
+ inputName: 'input_name',
};
let mockQueryResponse;
@@ -92,7 +94,7 @@ describe('NewProjectUrlSelect component', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findSelectedPath = () => wrapper.findComponent(GlTruncate);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
- const findHiddenNamespaceInput = () => wrapper.find('[name="project[namespace_id]"]');
+ const findHiddenNamespaceInput = () => wrapper.find(`[name="${defaultProvide.inputName}`);
const findHiddenSelectedNamespaceInput = () =>
wrapper.find('[name="project[selected_namespace_id]"]');
@@ -165,6 +167,8 @@ describe('NewProjectUrlSelect component', () => {
it("renders a hidden input with the user's namespace id", () => {
expect(findHiddenNamespaceInput().attributes('value')).toBe(defaultProvide.userNamespaceId);
+ expect(findHiddenNamespaceInput().attributes('name')).toBe(defaultProvide.inputName);
+ expect(findHiddenNamespaceInput().attributes('id')).toBe(defaultProvide.inputId);
});
it('renders a hidden input with the selected namespace id', () => {
@@ -198,6 +202,18 @@ describe('NewProjectUrlSelect component', () => {
expect(listItems.at(5).text()).toBe(data.currentUser.namespace.fullPath);
});
+ it('does not render users section when user namespace id is not provided', async () => {
+ wrapper = mountComponent({
+ mountFn: mount,
+ provide: { ...defaultProvide, userNamespaceId: null },
+ });
+
+ await showDropdown();
+
+ expect(wrapper.findAllComponents(GlDropdownSectionHeader)).toHaveLength(1);
+ expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(0).text()).toBe('Groups');
+ });
+
describe('query fetching', () => {
describe('on component mount', () => {
it('does not fetch query', () => {
@@ -297,7 +313,7 @@ describe('NewProjectUrlSelect component', () => {
);
});
- it('tracks clicking on the dropdown', () => {
+ it('tracks clicking on the dropdown when trackLabel is provided', () => {
wrapper = mountComponent();
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
@@ -311,4 +327,16 @@ describe('NewProjectUrlSelect component', () => {
unmockTracking();
});
+
+ it('does not track clicking on the dropdown when trackLabel is not provided', () => {
+ wrapper = mountComponent({ provide: { ...defaultProvide, trackLabel: null } });
+
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findDropdown().vm.$emit('show');
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+
+ unmockTracking();
+ });
});
diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js
index 4fcecc3a307..d69bfc4ec92 100644
--- a/spec/frontend/projects/project_new_spec.js
+++ b/spec/frontend/projects/project_new_spec.js
@@ -1,12 +1,14 @@
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import projectNew from '~/projects/project_new';
+import { checkRules } from '~/projects/project_name_rules';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
describe('New Project', () => {
let $projectImportUrl;
let $projectPath;
let $projectName;
+ let $projectNameError;
const mockKeyup = (el) => el.dispatchEvent(new KeyboardEvent('keyup'));
const mockChange = (el) => el.dispatchEvent(new Event('change'));
@@ -29,6 +31,7 @@ describe('New Project', () => {
</div>
</div>
<input id="project_name" />
+ <div class="gl-field-error hidden" id="project_name_error" />
<input id="project_path" />
</div>
<div class="js-user-readme-repo"></div>
@@ -41,6 +44,7 @@ describe('New Project', () => {
$projectImportUrl = document.querySelector('#project_import_url');
$projectPath = document.querySelector('#project_path');
$projectName = document.querySelector('#project_name');
+ $projectNameError = document.querySelector('#project_name_error');
});
afterEach(() => {
@@ -84,6 +88,57 @@ describe('New Project', () => {
});
});
+ describe('tracks manual name input', () => {
+ beforeEach(() => {
+ projectNew.bindEvents();
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('no error message by default', () => {
+ expect($projectNameError.classList.contains('hidden')).toBe(true);
+ });
+
+ it('show error message if name is validate', () => {
+ $projectName.value = '.validate!Name';
+ triggerEvent($projectName, 'change');
+
+ expect($projectNameError.innerText).toBe(
+ "Name must start with a letter, digit, emoji, or '_'",
+ );
+ expect($projectNameError.classList.contains('hidden')).toBe(false);
+ });
+ });
+
+ describe('project name rule', () => {
+ describe("Name must start with a letter, digit, emoji, or '_'", () => {
+ const errormsg = "Name must start with a letter, digit, emoji, or '_'";
+ it("'.foo' should error", () => {
+ const text = '.foo';
+ expect(checkRules(text)).toBe(errormsg);
+ });
+ it('_foo should passed', () => {
+ const text = '_foo';
+ expect(checkRules(text)).toBe('');
+ });
+ });
+
+ describe("Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces", () => {
+ const errormsg =
+ "Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces";
+ it("'foo(#^.^#)foo' should error", () => {
+ const text = 'foo(#^.^#)foo';
+ expect(checkRules(text)).toBe(errormsg);
+ });
+ it("'foo123😊_.+- ' should passed", () => {
+ const text = 'foo123😊_.+- ';
+ expect(checkRules(text)).toBe('');
+ });
+ });
+ });
+
describe('deriveProjectPathFromUrl', () => {
const dummyImportUrl = `${TEST_HOST}/dummy/import/url.git`;
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
index 27065a704e2..bc373d9deb7 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
@@ -16,10 +16,12 @@ import {
branchProtectionsMockResponse,
approvalRulesMock,
statusChecksRulesMock,
+ matchingBranchesCount,
} from './mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
getParameterByName: jest.fn().mockReturnValue('main'),
+ mergeUrlParams: jest.fn().mockReturnValue('/branches?state=all&search=main'),
joinPaths: jest.fn(),
}));
@@ -65,6 +67,13 @@ describe('View branch rules', () => {
const findForcePushTitle = () => wrapper.findByText(I18N.allowForcePushDescription);
const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle);
const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle);
+ const findMatchingBranchesLink = () =>
+ wrapper.findByText(
+ sprintf(I18N.matchingBranchesLinkTitle, {
+ total: matchingBranchesCount,
+ subject: 'branches',
+ }),
+ );
it('gets the branch param from url and renders it in the view', () => {
expect(util.getParameterByName).toHaveBeenCalledWith('branch');
@@ -85,6 +94,12 @@ describe('View branch rules', () => {
expect(findBranchTitle().exists()).toBe(true);
});
+ it('renders matching branches link', () => {
+ const matchingBranchesLink = findMatchingBranchesLink();
+ expect(matchingBranchesLink.exists()).toBe(true);
+ expect(matchingBranchesLink.attributes().href).toBe('/branches?state=all&search=main');
+ });
+
it('renders a branch protection title', () => {
expect(findBranchProtectionTitle().exists()).toBe(true);
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
index c07d4673344..821dba75b62 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
@@ -109,6 +109,8 @@ export const accessLevelsMockResponse = [
},
];
+export const matchingBranchesCount = 3;
+
export const branchProtectionsMockResponse = {
data: {
project: {
@@ -141,6 +143,7 @@ export const branchProtectionsMockResponse = {
__typename: 'ExternalStatusCheckConnection',
nodes: statusChecksRulesMock,
},
+ matchingBranchesCount,
},
{
__typename: 'BranchRule',
@@ -166,6 +169,7 @@ export const branchProtectionsMockResponse = {
__typename: 'ExternalStatusCheckConnection',
nodes: [],
},
+ matchingBranchesCount,
},
],
},
diff --git a/spec/frontend/projects/settings/mock_data.js b/spec/frontend/projects/settings/mock_data.js
new file mode 100644
index 00000000000..0262c0e3e43
--- /dev/null
+++ b/spec/frontend/projects/settings/mock_data.js
@@ -0,0 +1,57 @@
+const accessLevelsMockResponse = [
+ {
+ __typename: 'PushAccessLevelEdge',
+ node: {
+ __typename: 'PushAccessLevel',
+ accessLevel: 40,
+ accessLevelDescription: 'Jona Langworth',
+ group: null,
+ user: {
+ __typename: 'UserCore',
+ id: '123',
+ webUrl: 'test.com',
+ name: 'peter',
+ avatarUrl: 'test.com/user.png',
+ },
+ },
+ },
+ {
+ __typename: 'PushAccessLevelEdge',
+ node: {
+ __typename: 'PushAccessLevel',
+ accessLevel: 40,
+ accessLevelDescription: 'Maintainers',
+ group: null,
+ user: null,
+ },
+ },
+];
+
+export const pushAccessLevelsMockResponse = {
+ __typename: 'PushAccessLevelConnection',
+ edges: accessLevelsMockResponse,
+};
+
+export const pushAccessLevelsMockResult = {
+ total: 2,
+ users: [
+ {
+ src: 'test.com/user.png',
+ __typename: 'UserCore',
+ id: '123',
+ webUrl: 'test.com',
+ name: 'peter',
+ avatarUrl: 'test.com/user.png',
+ },
+ ],
+ groups: [],
+ roles: [
+ {
+ __typename: 'PushAccessLevel',
+ accessLevel: 40,
+ accessLevelDescription: 'Maintainers',
+ group: null,
+ user: null,
+ },
+ ],
+};
diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
index 6369f04781f..447d7e86ceb 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -5,9 +5,12 @@ import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BranchRules, { i18n } from '~/projects/settings/repository/branch_rules/app.vue';
import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
-import branchRulesQuery from '~/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
+import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
import { createAlert } from '~/flash';
-import { branchRulesMockResponse, appProvideMock } from './mock_data';
+import {
+ branchRulesMockResponse,
+ appProvideMock,
+} from 'ee_else_ce_jest/projects/settings/repository/branch_rules/mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
index 2aa93fd0e28..49c45c080b4 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
@@ -50,17 +50,15 @@ describe('Branch rule', () => {
it('renders the protection details list items', () => {
expect(findProtectionDetailsListItems()).toHaveLength(wrapper.vm.approvalDetails.length);
expect(findProtectionDetailsListItems().at(0).text()).toBe(i18n.allowForcePush);
- expect(findProtectionDetailsListItems().at(1).text()).toBe(i18n.codeOwnerApprovalRequired);
- expect(findProtectionDetailsListItems().at(2).text()).toMatchInterpolatedText(
- sprintf(i18n.statusChecks, {
- total: branchRulePropsMock.statusChecksTotal,
- subject: n__('check', 'checks', branchRulePropsMock.statusChecksTotal),
- }),
- );
- expect(findProtectionDetailsListItems().at(3).text()).toMatchInterpolatedText(
- sprintf(i18n.approvalRules, {
- total: branchRulePropsMock.approvalRulesTotal,
- subject: n__('rule', 'rules', branchRulePropsMock.approvalRulesTotal),
+ expect(findProtectionDetailsListItems().at(1).text()).toBe(wrapper.vm.pushAccessLevelsText);
+ });
+
+ it('renders branches count for wildcards', () => {
+ createComponent({ name: 'test-*' });
+ expect(findProtectionDetailsListItems().at(0).text()).toMatchInterpolatedText(
+ sprintf(i18n.matchingBranches, {
+ total: branchRulePropsMock.matchingBranchesCount,
+ subject: n__('branch', 'branches', branchRulePropsMock.matchingBranchesCount),
}),
);
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
index 8aa03a12996..6f506882c36 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
@@ -1,3 +1,22 @@
+export const accessLevelsMockResponse = [
+ {
+ __typename: 'PushAccessLevelEdge',
+ node: {
+ __typename: 'PushAccessLevel',
+ accessLevel: 40,
+ accessLevelDescription: 'Developers',
+ },
+ },
+ {
+ __typename: 'PushAccessLevelEdge',
+ node: {
+ __typename: 'PushAccessLevel',
+ accessLevel: 40,
+ accessLevelDescription: 'Maintainers',
+ },
+ },
+];
+
export const branchRulesMockResponse = {
data: {
project: {
@@ -9,34 +28,34 @@ export const branchRulesMockResponse = {
{
name: 'main',
isDefault: true,
+ matchingBranchesCount: 1,
branchProtection: {
allowForcePush: true,
- codeOwnerApprovalRequired: true,
- },
- approvalRules: {
- nodes: [{ id: 1 }],
- __typename: 'ApprovalProjectRuleConnection',
- },
- externalStatusChecks: {
- nodes: [{ id: 1 }, { id: 2 }],
- __typename: 'BranchRule',
+ mergeAccessLevels: {
+ edges: [],
+ __typename: 'MergeAccessLevelConnection',
+ },
+ pushAccessLevels: {
+ edges: accessLevelsMockResponse,
+ __typename: 'PushAccessLevelConnection',
+ },
},
__typename: 'BranchRule',
},
{
name: 'test-*',
isDefault: false,
+ matchingBranchesCount: 2,
branchProtection: {
allowForcePush: false,
- codeOwnerApprovalRequired: false,
- },
- approvalRules: {
- nodes: [],
- __typename: 'ApprovalProjectRuleConnection',
- },
- externalStatusChecks: {
- nodes: [],
- __typename: 'BranchRule',
+ mergeAccessLevels: {
+ edges: [],
+ __typename: 'MergeAccessLevelConnection',
+ },
+ pushAccessLevels: {
+ edges: [],
+ __typename: 'PushAccessLevelConnection',
+ },
},
__typename: 'BranchRule',
},
@@ -57,17 +76,22 @@ export const branchRuleProvideMock = {
export const branchRulePropsMock = {
name: 'main',
isDefault: true,
+ matchingBranchesCount: 1,
branchProtection: {
allowForcePush: true,
- codeOwnerApprovalRequired: true,
+ codeOwnerApprovalRequired: false,
+ pushAccessLevels: {
+ edges: accessLevelsMockResponse,
+ },
},
- approvalRulesTotal: 1,
- statusChecksTotal: 2,
+ approvalRulesTotal: 0,
+ statusChecksTotal: 0,
};
export const branchRuleWithoutDetailsPropsMock = {
- name: 'main',
+ name: 'branch-1',
isDefault: false,
+ matchingBranchesCount: 1,
branchProtection: {
allowForcePush: false,
codeOwnerApprovalRequired: false,
diff --git a/spec/frontend/projects/settings/utils_spec.js b/spec/frontend/projects/settings/utils_spec.js
new file mode 100644
index 00000000000..319aa4000b5
--- /dev/null
+++ b/spec/frontend/projects/settings/utils_spec.js
@@ -0,0 +1,11 @@
+import { getAccessLevels } from '~/projects/settings/utils';
+import { pushAccessLevelsMockResponse, pushAccessLevelsMockResult } from './mock_data';
+
+describe('Utils', () => {
+ describe('getAccessLevels', () => {
+ it('takes accessLevels response data and returns acecssLevels object', () => {
+ const pushAccessLevels = getAccessLevels(pushAccessLevelsMockResponse);
+ expect(pushAccessLevels).toEqual(pushAccessLevelsMockResult);
+ });
+ });
+});
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap
index d88d79d2cde..00fc521b716 100644
--- a/spec/frontend/releases/__snapshots__/util_spec.js.snap
+++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap
@@ -43,16 +43,17 @@ Object {
},
"author": Object {
"__typename": "UserCore",
- "avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon",
+ "avatarUrl": "https://www.gravatar.com/avatar/eb329fbfeccd9e6d45ff159da8736876?s=80&d=identicon",
"id": Any<String>,
- "username": "administrator",
- "webUrl": "http://localhost/administrator",
+ "username": "user1",
+ "webUrl": "http://localhost/user1",
},
"commit": Object {
"shortId": "b83d6e39",
"title": "Merge branch 'branch-merged' into 'master'",
},
"commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
+ "createdAt": 2019-01-03T00:00:00.000Z,
"descriptionHtml": "<p data-sourcepos=\\"1:1-1:23\\" dir=\\"auto\\">An okay release <gl-emoji title=\\"shrug\\" data-name=\\"shrug\\" data-unicode-version=\\"9.0\\">🤷</gl-emoji></p>",
"evidences": Array [],
"historicalRelease": false,
@@ -140,16 +141,17 @@ Object {
},
"author": Object {
"__typename": "UserCore",
- "avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon",
+ "avatarUrl": "https://www.gravatar.com/avatar/eb329fbfeccd9e6d45ff159da8736876?s=80&d=identicon",
"id": Any<String>,
- "username": "administrator",
- "webUrl": "http://localhost/administrator",
+ "username": "user1",
+ "webUrl": "http://localhost/user1",
},
"commit": Object {
"shortId": "b83d6e39",
"title": "Merge branch 'branch-merged' into 'master'",
},
"commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
+ "createdAt": 2018-12-03T00:00:00.000Z,
"descriptionHtml": "<p data-sourcepos=\\"1:1-1:33\\" dir=\\"auto\\">Best. Release. <strong>Ever.</strong> <gl-emoji title=\\"rocket\\" data-name=\\"rocket\\" data-unicode-version=\\"6.0\\">🚀</gl-emoji></p>",
"evidences": Array [
Object {
@@ -253,6 +255,7 @@ Object {
"sources": Array [],
},
"author": undefined,
+ "createdAt": 2018-12-03T00:00:00.000Z,
"description": "Best. Release. **Ever.** :rocket:",
"evidences": Array [],
"milestones": Array [
@@ -362,16 +365,17 @@ Object {
},
"author": Object {
"__typename": "UserCore",
- "avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon",
+ "avatarUrl": "https://www.gravatar.com/avatar/eb329fbfeccd9e6d45ff159da8736876?s=80&d=identicon",
"id": Any<String>,
- "username": "administrator",
- "webUrl": "http://localhost/administrator",
+ "username": "user1",
+ "webUrl": "http://localhost/user1",
},
"commit": Object {
"shortId": "b83d6e39",
"title": "Merge branch 'branch-merged' into 'master'",
},
"commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
+ "createdAt": 2018-12-03T00:00:00.000Z,
"descriptionHtml": "<p data-sourcepos=\\"1:1-1:33\\" dir=\\"auto\\">Best. Release. <strong>Ever.</strong> <gl-emoji title=\\"rocket\\" data-name=\\"rocket\\" data-unicode-version=\\"6.0\\">🚀</gl-emoji></p>",
"evidences": Array [
Object {
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index 8f4efad197f..19b41d05a44 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -4,6 +4,7 @@ import { cloneDeep } from 'lodash';
import { nextTick } from 'vue';
import originalOneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json';
import { convertOneReleaseGraphQLResponse } from '~/releases/util';
+import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants';
import { trimText } from 'helpers/text_helper';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
@@ -43,88 +44,118 @@ describe('Release block footer', () => {
const tagInfoSectionLink = () => tagInfoSection().findComponent(GlLink);
const authorDateInfoSection = () => wrapper.find('.js-author-date-info');
- describe('with all props provided', () => {
- beforeEach(() => factory());
-
- it('renders the commit icon', () => {
- const commitIcon = commitInfoSection().findComponent(GlIcon);
-
- expect(commitIcon.exists()).toBe(true);
- expect(commitIcon.props('name')).toBe('commit');
- });
-
- it('renders the commit SHA with a link', () => {
- const commitLink = commitInfoSectionLink();
-
- expect(commitLink.exists()).toBe(true);
- expect(commitLink.text()).toBe(release.commit.shortId);
- expect(commitLink.attributes('href')).toBe(release.commitPath);
- });
-
- it('renders the tag icon', () => {
- const commitIcon = tagInfoSection().findComponent(GlIcon);
-
- expect(commitIcon.exists()).toBe(true);
- expect(commitIcon.props('name')).toBe('tag');
- });
-
- it('renders the tag name with a link', () => {
- const commitLink = tagInfoSection().findComponent(GlLink);
-
- expect(commitLink.exists()).toBe(true);
- expect(commitLink.text()).toBe(release.tagName);
- expect(commitLink.attributes('href')).toBe(release.tagPath);
- });
-
- it('renders the author and creation time info', () => {
- expect(trimText(authorDateInfoSection().text())).toBe(
- `Created 1 year ago by ${release.author.username}`,
- );
- });
-
- describe('when the release date is in the past', () => {
- it('prefixes the creation info with "Created"', () => {
- expect(trimText(authorDateInfoSection().text())).toEqual(expect.stringMatching(/^Created/));
- });
- });
-
- describe('renders the author and creation time info with future release date', () => {
- beforeEach(() => {
- factory({ releasedAt: mockFutureDate });
- });
-
- it('renders the release date without the author name', () => {
- expect(trimText(authorDateInfoSection().text())).toBe(
- `Will be created in 1 month by ${release.author.username}`,
- );
- });
- });
-
- describe('when the release date is in the future', () => {
- beforeEach(() => {
- factory({ releasedAt: mockFutureDate });
- });
-
- it('prefixes the creation info with "Will be created"', () => {
- expect(trimText(authorDateInfoSection().text())).toEqual(
- expect.stringMatching(/^Will be created/),
- );
- });
- });
-
- it("renders the author's avatar image", () => {
- const avatarImg = authorDateInfoSection().find('img');
-
- expect(avatarImg.exists()).toBe(true);
- expect(avatarImg.attributes('src')).toBe(release.author.avatarUrl);
- });
-
- it("renders a link to the author's profile", () => {
- const authorLink = authorDateInfoSection().findComponent(GlLink);
-
- expect(authorLink.exists()).toBe(true);
- expect(authorLink.attributes('href')).toBe(release.author.webUrl);
- });
+ describe.each`
+ sortFlag | expectedInfoString
+ ${null} | ${'Created'}
+ ${CREATED_ASC} | ${'Created'}
+ ${CREATED_DESC} | ${'Created'}
+ ${RELEASED_AT_ASC} | ${'Released'}
+ ${RELEASED_AT_DESC} | ${'Released'}
+ `('with sorting set to $sortFlag', ({ sortFlag, expectedInfoString }) => {
+ const dateAt =
+ expectedInfoString === 'Created' ? originalRelease.createdAt : originalRelease.releasedAt;
+
+ describe.each`
+ dateType | dateFlag | expectedInfoStringPrefix | expectedDateString
+ ${'empty'} | ${undefined} | ${null} | ${null}
+ ${'in the past'} | ${dateAt} | ${null} | ${'1 year ago'}
+ ${'in the future'} | ${mockFutureDate} | ${'Will be'} | ${'in 1 month'}
+ `(
+ 'with date set to $dateType',
+ ({ dateFlag, expectedInfoStringPrefix, expectedDateString }) => {
+ describe.each`
+ authorType | authorFlag | expectedAuthorString
+ ${'empty'} | ${undefined} | ${null}
+ ${'present'} | ${originalRelease.author} | ${'by user1'}
+ `('with author set to $authorType', ({ authorFlag, expectedAuthorString }) => {
+ const propsData = { sort: sortFlag, author: authorFlag };
+ if (dateFlag !== '') {
+ propsData.createdAt = dateFlag;
+ propsData.releasedAt = dateFlag;
+ }
+
+ beforeEach(() => {
+ factory({ ...propsData });
+ });
+
+ const expectedString = [
+ expectedInfoStringPrefix,
+ expectedInfoStringPrefix ? expectedInfoString.toLowerCase() : expectedInfoString,
+ expectedDateString,
+ expectedAuthorString,
+ ];
+
+ if (authorFlag || dateFlag) {
+ it('renders the author and creation time info', () => {
+ expect(trimText(authorDateInfoSection().text())).toBe(
+ expectedString.filter((n) => n).join(' '),
+ );
+ });
+ if (authorFlag) {
+ it("renders the author's avatar image", () => {
+ const avatarImg = authorDateInfoSection().find('img');
+
+ expect(avatarImg.exists()).toBe(true);
+ expect(avatarImg.attributes('src')).toBe(release.author.avatarUrl);
+ });
+
+ it("renders a link to the author's profile", () => {
+ const authorLink = authorDateInfoSection().findComponent(GlLink);
+
+ expect(authorLink.exists()).toBe(true);
+ expect(authorLink.attributes('href')).toBe(release.author.webUrl);
+ });
+ } else {
+ it("does not render the author's avatar image", () => {
+ const avatarImg = authorDateInfoSection().find('img');
+
+ expect(avatarImg.exists()).toBe(false);
+ });
+
+ it("does not render a link to the author's profile", () => {
+ const authorLink = authorDateInfoSection().findComponent(GlLink);
+
+ expect(authorLink.exists()).toBe(false);
+ });
+ }
+ } else {
+ it('does not render the author and creation time info', () => {
+ expect(authorDateInfoSection().exists()).toBe(false);
+ });
+ }
+
+ it('renders the commit icon', () => {
+ const commitIcon = commitInfoSection().findComponent(GlIcon);
+
+ expect(commitIcon.exists()).toBe(true);
+ expect(commitIcon.props('name')).toBe('commit');
+ });
+
+ it('renders the commit SHA with a link', () => {
+ const commitLink = commitInfoSectionLink();
+
+ expect(commitLink.exists()).toBe(true);
+ expect(commitLink.text()).toBe(release.commit.shortId);
+ expect(commitLink.attributes('href')).toBe(release.commitPath);
+ });
+
+ it('renders the tag icon', () => {
+ const commitIcon = tagInfoSection().findComponent(GlIcon);
+
+ expect(commitIcon.exists()).toBe(true);
+ expect(commitIcon.props('name')).toBe('tag');
+ });
+
+ it('renders the tag name with a link', () => {
+ const commitLink = tagInfoSection().findComponent(GlLink);
+
+ expect(commitLink.exists()).toBe(true);
+ expect(commitLink.text()).toBe(release.tagName);
+ expect(commitLink.attributes('href')).toBe(release.tagPath);
+ });
+ });
+ },
+ );
});
describe('without any commit info', () => {
@@ -160,40 +191,4 @@ describe('Release block footer', () => {
expect(tagInfoSection().text()).toBe(release.tagName);
});
});
-
- describe('without any author info', () => {
- beforeEach(() => factory({ author: undefined }));
-
- it('renders the release date without the author name', () => {
- expect(trimText(authorDateInfoSection().text())).toBe(`Created 1 year ago`);
- });
- });
-
- describe('future release without any author info', () => {
- beforeEach(() => {
- factory({ author: undefined, releasedAt: mockFutureDate });
- });
-
- it('renders the release date without the author name', () => {
- expect(trimText(authorDateInfoSection().text())).toBe(`Will be created in 1 month`);
- });
- });
-
- describe('without a released at date', () => {
- beforeEach(() => factory({ releasedAt: undefined }));
-
- it('renders the author name without the release date', () => {
- expect(trimText(authorDateInfoSection().text())).toBe(
- `Created by ${release.author.username}`,
- );
- });
- });
-
- describe('without a release date or author info', () => {
- beforeEach(() => factory({ author: undefined, releasedAt: undefined }));
-
- it('does not render any author or release date info', () => {
- expect(authorDateInfoSection().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index 096c3db8902..f1b8554fbc3 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -1,5 +1,4 @@
import { mount } from '@vue/test-utils';
-import $ from 'jquery';
import { nextTick } from 'vue';
import originalOneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json';
import { convertOneReleaseGraphQLResponse } from '~/releases/util';
@@ -10,6 +9,9 @@ import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
describe('Release block', () => {
let wrapper;
@@ -34,7 +36,6 @@ describe('Release block', () => {
const editButton = () => wrapper.find('.js-edit-button');
beforeEach(() => {
- jest.spyOn($.fn, 'renderGFM');
release = convertOneReleaseGraphQLResponse(originalOneReleaseQueryResponse).data;
});
@@ -62,7 +63,7 @@ describe('Release block', () => {
it('renders release description', () => {
expect(wrapper.vm.$refs['gfm-content']).toBeDefined();
- expect($.fn.renderGFM).toHaveBeenCalledTimes(1);
+ expect(renderGFM).toHaveBeenCalledTimes(1);
});
it('renders release date', () => {
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index 2180f78a8df..8b987551b33 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -82,9 +82,6 @@ function factory({ path, isLoading = false, hasMore = true, entries = {}, commit
mocks: {
$apollo,
},
- provide: {
- glFeatures: { lazyLoadCommits: true },
- },
});
}
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 64aa6d179a8..5d9138ab9cd 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -30,9 +30,6 @@ function factory(propsData = {}) {
directives: {
GlHoverLoad: createMockDirective(),
},
- provide: {
- glFeatures: { lazyLoadCommits: true },
- },
mocks: {
$router,
},
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index 352f4314232..6eea66f1a7d 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -31,7 +31,6 @@ function factory(path, data = () => ({})) {
glFeatures: {
increasePageSizeExponentially: true,
paginatedTreeGraphqlQuery: true,
- lazyLoadCommits: true,
},
},
});
diff --git a/spec/frontend/repository/utils/ref_switcher_utils_spec.js b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
new file mode 100644
index 00000000000..3335059554f
--- /dev/null
+++ b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
@@ -0,0 +1,22 @@
+import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
+
+const projectRootPath = 'root/Project1';
+const currentRef = 'main';
+const selectedRef = 'feature';
+
+describe('generateRefDestinationPath', () => {
+ it.each`
+ currentPath | result
+ ${projectRootPath} | ${`${projectRootPath}/-/tree/${selectedRef}`}
+ ${`${projectRootPath}/-/tree/${currentRef}/dir1`} | ${`${projectRootPath}/-/tree/${selectedRef}/dir1`}
+ ${`${projectRootPath}/-/tree/${currentRef}/dir1/dir2`} | ${`${projectRootPath}/-/tree/${selectedRef}/dir1/dir2`}
+ ${`${projectRootPath}/-/blob/${currentRef}/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/test.js`}
+ ${`${projectRootPath}/-/blob/${currentRef}/dir1/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/test.js`}
+ ${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js`}
+ ${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js#L123`}
+ `('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
+ setWindowLocation(currentPath);
+ expect(generateRefDestinationPath(projectRootPath, selectedRef)).toBe(result);
+ });
+});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index fa5ccfeb478..e02d3b0eab8 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -114,6 +114,7 @@ export const MOCK_NAVIGATION = {
scope: 'projects',
link: '/search?scope=projects&search=et',
count_link: '/search/count?scope=projects&search=et',
+ count: '10,000+',
},
blobs: {
label: 'Code',
diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
index c57eabd57b9..d5ecca4636c 100644
--- a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
@@ -1,39 +1,16 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { MOCK_QUERY } from 'jest/search/mock_data';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
-Vue.use(Vuex);
-
describe('ConfidentialityFilter', () => {
let wrapper;
- const actionSpies = {
- applyQuery: jest.fn(),
- resetQuery: jest.fn(),
- };
-
- const createComponent = (initialState) => {
- const store = new Vuex.Store({
- state: {
- query: MOCK_QUERY,
- ...initialState,
- },
- actions: actionSpies,
- });
-
+ const createComponent = (initProps) => {
wrapper = shallowMount(ConfidentialityFilter, {
- store,
+ ...initProps,
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findRadioFilter = () => wrapper.findComponent(RadioFilter);
describe('template', () => {
@@ -41,24 +18,28 @@ describe('ConfidentialityFilter', () => {
createComponent();
});
- describe.each`
- scope | showFilter
- ${'issues'} | ${true}
- ${'merge_requests'} | ${false}
- ${'projects'} | ${false}
- ${'milestones'} | ${false}
- ${'users'} | ${false}
- ${'notes'} | ${false}
- ${'wiki_blobs'} | ${false}
- ${'blobs'} | ${false}
- `(`dropdown`, ({ scope, showFilter }) => {
- beforeEach(() => {
- createComponent({ query: { scope } });
- });
+ it('renders the component', () => {
+ expect(findRadioFilter().exists()).toBe(true);
+ });
+ });
- it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
- expect(findRadioFilter().exists()).toBe(showFilter);
+ describe.each`
+ hasFeatureFlagEnabled | paddingClass
+ ${true} | ${'gl-px-5'}
+ ${false} | ${'gl-px-0'}
+ `(`RadioFilter`, ({ hasFeatureFlagEnabled, paddingClass }) => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: {
+ searchPageVerticalNav: hasFeatureFlagEnabled,
+ },
+ },
});
});
+
+ it(`has ${paddingClass} class`, () => {
+ expect(findRadioFilter().classes(paddingClass)).toBe(true);
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/filters_spec.js b/spec/frontend/search/sidebar/components/filters_spec.js
index 4f217709297..7e564bfa005 100644
--- a/spec/frontend/search/sidebar/components/filters_spec.js
+++ b/spec/frontend/search/sidebar/components/filters_spec.js
@@ -129,4 +129,44 @@ describe('GlobalSearchSidebarFilters', () => {
expect(actionSpies.resetQuery).toHaveBeenCalled();
});
});
+
+ describe.each`
+ scope | showFilter
+ ${'issues'} | ${true}
+ ${'merge_requests'} | ${false}
+ ${'projects'} | ${false}
+ ${'milestones'} | ${false}
+ ${'users'} | ${false}
+ ${'notes'} | ${false}
+ ${'wiki_blobs'} | ${false}
+ ${'blobs'} | ${false}
+ `(`ConfidentialityFilter`, ({ scope, showFilter }) => {
+ beforeEach(() => {
+ createComponent({ urlQuery: { scope } });
+ });
+
+ it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
+ expect(findConfidentialityFilter().exists()).toBe(showFilter);
+ });
+ });
+
+ describe.each`
+ scope | showFilter
+ ${'issues'} | ${true}
+ ${'merge_requests'} | ${true}
+ ${'projects'} | ${false}
+ ${'milestones'} | ${false}
+ ${'users'} | ${false}
+ ${'notes'} | ${false}
+ ${'wiki_blobs'} | ${false}
+ ${'blobs'} | ${false}
+ `(`StatusFilter`, ({ scope, showFilter }) => {
+ beforeEach(() => {
+ createComponent({ urlQuery: { scope } });
+ });
+
+ it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
+ expect(findStatusFilter().exists()).toBe(showFilter);
+ });
+ });
});
diff --git a/spec/frontend/search/sidebar/components/scope_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
index 6262a52e01a..23c158239dc 100644
--- a/spec/frontend/search/sidebar/components/scope_navigation_spec.js
+++ b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
@@ -1,4 +1,4 @@
-import { GlNav, GlNavItem } from '@gitlab/ui';
+import { GlNav, GlNavItem, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -37,6 +37,7 @@ describe('ScopeNavigation', () => {
const findGlNav = () => wrapper.findComponent(GlNav);
const findGlNavItems = () => wrapper.findAllComponents(GlNavItem);
const findGlNavItemActive = () => findGlNavItems().wrappers.filter((w) => w.attributes('active'));
+ const findGlNavItemActiveLabel = () => findGlNavItemActive().at(0).findAll('span').at(0).text();
const findGlNavItemActiveCount = () => findGlNavItemActive().at(0).findAll('span').at(1);
describe('scope navigation', () => {
@@ -56,7 +57,7 @@ describe('ScopeNavigation', () => {
expect(findGlNavItems()).toHaveLength(9);
});
- it('nav items have proper links', () => {
+ it('has all proper links', () => {
const linkAtPosition = 3;
const { link } = MOCK_NAVIGATION[Object.keys(MOCK_NAVIGATION)[linkAtPosition]];
@@ -64,17 +65,47 @@ describe('ScopeNavigation', () => {
});
});
- describe('scope navigation sets proper state', () => {
+ describe('scope navigation sets proper state with url scope set', () => {
beforeEach(() => {
createComponent();
});
- it('sets proper class to active item', () => {
+ it('has correct active item', () => {
expect(findGlNavItemActive()).toHaveLength(1);
+ expect(findGlNavItemActiveLabel()).toBe('Issues');
});
- it('active item', () => {
+ it('has correct active item count', () => {
expect(findGlNavItemActiveCount().text()).toBe('2.4K');
});
+
+ it('does not have plus sign after count text', () => {
+ expect(findGlNavItemActive().at(0).findComponent(GlIcon).exists()).toBe(false);
+ });
+
+ it('has count is highlighted correctly', () => {
+ expect(findGlNavItemActiveCount().classes('gl-text-gray-900')).toBe(true);
+ });
+ });
+
+ describe('scope navigation sets proper state with NO url scope set', () => {
+ beforeEach(() => {
+ createComponent({
+ urlQuery: {},
+ });
+ });
+
+ it('has correct active item', () => {
+ expect(findGlNavItems().at(0).attributes('active')).toBe('true');
+ expect(findGlNavItemActiveLabel()).toBe('Projects');
+ });
+
+ it('has correct active item count', () => {
+ expect(findGlNavItemActiveCount().text()).toBe('10K');
+ });
+
+ it('has correct active item count and over limit sign', () => {
+ expect(findGlNavItemActive().at(0).findComponent(GlIcon).exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js
index f3152c014b6..2ed199469e6 100644
--- a/spec/frontend/search/sidebar/components/status_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/status_filter_spec.js
@@ -1,39 +1,16 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { MOCK_QUERY } from 'jest/search/mock_data';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
import StatusFilter from '~/search/sidebar/components/status_filter.vue';
-Vue.use(Vuex);
-
describe('StatusFilter', () => {
let wrapper;
- const actionSpies = {
- applyQuery: jest.fn(),
- resetQuery: jest.fn(),
- };
-
- const createComponent = (initialState) => {
- const store = new Vuex.Store({
- state: {
- query: MOCK_QUERY,
- ...initialState,
- },
- actions: actionSpies,
- });
-
+ const createComponent = (initProps) => {
wrapper = shallowMount(StatusFilter, {
- store,
+ ...initProps,
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findRadioFilter = () => wrapper.findComponent(RadioFilter);
describe('template', () => {
@@ -41,24 +18,28 @@ describe('StatusFilter', () => {
createComponent();
});
- describe.each`
- scope | showFilter
- ${'issues'} | ${true}
- ${'merge_requests'} | ${true}
- ${'projects'} | ${false}
- ${'milestones'} | ${false}
- ${'users'} | ${false}
- ${'notes'} | ${false}
- ${'wiki_blobs'} | ${false}
- ${'blobs'} | ${false}
- `(`dropdown`, ({ scope, showFilter }) => {
- beforeEach(() => {
- createComponent({ query: { scope } });
- });
+ it('renders the component', () => {
+ expect(findRadioFilter().exists()).toBe(true);
+ });
+ });
- it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
- expect(findRadioFilter().exists()).toBe(showFilter);
+ describe.each`
+ hasFeatureFlagEnabled | paddingClass
+ ${true} | ${'gl-px-5'}
+ ${false} | ${'gl-px-0'}
+ `(`RadioFilter`, ({ hasFeatureFlagEnabled, paddingClass }) => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: {
+ searchPageVerticalNav: hasFeatureFlagEnabled,
+ },
+ },
});
});
+
+ it(`has ${paddingClass} class`, () => {
+ expect(findRadioFilter().classes(paddingClass)).toBe(true);
+ });
});
});
diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js
index c7fd7084101..3975887cfff 100644
--- a/spec/frontend/search/topbar/components/app_spec.js
+++ b/spec/frontend/search/topbar/components/app_spec.js
@@ -1,4 +1,4 @@
-import { GlSearchBoxByClick } from '@gitlab/ui';
+import { GlSearchBoxByClick, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -6,6 +6,8 @@ import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchTopbar from '~/search/topbar/components/app.vue';
import GroupFilter from '~/search/topbar/components/group_filter.vue';
import ProjectFilter from '~/search/topbar/components/project_filter.vue';
+import MarkdownDrawer from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
+import { SYNTAX_OPTIONS_DOCUMENT } from '~/search/topbar/constants';
Vue.use(Vuex);
@@ -18,7 +20,7 @@ describe('GlobalSearchTopbar', () => {
preloadStoredFrequentItems: jest.fn(),
};
- const createComponent = (initialState) => {
+ const createComponent = (initialState, props, stubs) => {
const store = new Vuex.Store({
state: {
query: MOCK_QUERY,
@@ -29,6 +31,8 @@ describe('GlobalSearchTopbar', () => {
wrapper = shallowMount(GlobalSearchTopbar, {
store,
+ propsData: props,
+ stubs,
});
};
@@ -39,6 +43,8 @@ describe('GlobalSearchTopbar', () => {
const findGlSearchBox = () => wrapper.findComponent(GlSearchBoxByClick);
const findGroupFilter = () => wrapper.findComponent(GroupFilter);
const findProjectFilter = () => wrapper.findComponent(ProjectFilter);
+ const findSyntaxOptionButton = () => wrapper.findComponent(GlButton);
+ const findSyntaxOptionDrawer = () => wrapper.findComponent(MarkdownDrawer);
describe('template', () => {
beforeEach(() => {
@@ -71,6 +77,72 @@ describe('GlobalSearchTopbar', () => {
expect(findProjectFilter().exists()).toBe(showFilters);
});
});
+
+ describe('syntax option feature', () => {
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent(
+ { query: { repository_ref: '' } },
+ { elasticsearchEnabled: true, defaultBranchName: '' },
+ );
+ });
+
+ it('renders button correctly', () => {
+ expect(findSyntaxOptionButton().exists()).toBe(true);
+ });
+
+ it('renders drawer correctly', () => {
+ expect(findSyntaxOptionDrawer().exists()).toBe(true);
+ expect(findSyntaxOptionDrawer().attributes('documentpath')).toBe(SYNTAX_OPTIONS_DOCUMENT);
+ });
+
+ it('dispatched correct click action', () => {
+ const draweToggleSpy = jest.fn();
+ wrapper.vm.$refs.markdownDrawer.toggleDrawer = draweToggleSpy;
+
+ findSyntaxOptionButton().vm.$emit('click');
+ expect(draweToggleSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe.each`
+ query | propsData | hasSyntaxOptions
+ ${null} | ${{ elasticsearchEnabled: false, defaultBranchName: '' }} | ${false}
+ ${{ query: { repository_ref: '' } }} | ${{ elasticsearchEnabled: false, defaultBranchName: '' }} | ${false}
+ ${{ query: { repository_ref: 'master' } }} | ${{ elasticsearchEnabled: false, defaultBranchName: 'master' }} | ${false}
+ ${{ query: { repository_ref: 'master' } }} | ${{ elasticsearchEnabled: true, defaultBranchName: '' }} | ${false}
+ ${{ query: { repository_ref: '' } }} | ${{ elasticsearchEnabled: true, defaultBranchName: 'master' }} | ${true}
+ ${{ query: { repository_ref: '' } }} | ${{ elasticsearchEnabled: true, defaultBranchName: '' }} | ${true}
+ ${{ query: { repository_ref: 'master' } }} | ${{ elasticsearchEnabled: true, defaultBranchName: 'master' }} | ${true}
+ `(
+ 'renders the syntax option based on component state',
+ ({ query, propsData, hasSyntaxOptions }) => {
+ beforeEach(() => {
+ createComponent(query, { ...propsData });
+ });
+
+ it(`does${
+ hasSyntaxOptions ? '' : ' not'
+ } have syntax option button when repository_ref: '${
+ query?.query?.repository_ref
+ }', elasticsearchEnabled: ${propsData.elasticsearchEnabled}, defaultBranchName: '${
+ propsData.defaultBranchName
+ }'`, () => {
+ expect(findSyntaxOptionButton().exists()).toBe(hasSyntaxOptions);
+ });
+
+ it(`does${
+ hasSyntaxOptions ? '' : ' not'
+ } have syntax option drawer when repository_ref: '${
+ query?.query?.repository_ref
+ }', elasticsearchEnabled: ${propsData.elasticsearchEnabled}, defaultBranchName: '${
+ propsData.defaultBranchName
+ }'`, () => {
+ expect(findSyntaxOptionDrawer().exists()).toBe(hasSyntaxOptions);
+ });
+ },
+ );
+ });
});
describe('actions', () => {
diff --git a/spec/frontend/self_monitor/store/actions_spec.js b/spec/frontend/self_monitor/store/actions_spec.js
index 21e63533c66..65c9d2f5f01 100644
--- a/spec/frontend/self_monitor/store/actions_spec.js
+++ b/spec/frontend/self_monitor/store/actions_spec.js
@@ -1,7 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import statusCodes from '~/lib/utils/http_status';
+import statusCodes, { HTTP_STATUS_ACCEPTED } from '~/lib/utils/http_status';
import * as actions from '~/self_monitor/store/actions';
import * as types from '~/self_monitor/store/mutation_types';
import createState from '~/self_monitor/store/state';
@@ -44,7 +44,7 @@ describe('self-monitor actions', () => {
beforeEach(() => {
state.createProjectEndpoint = '/create';
state.createProjectStatusEndpoint = '/create_status';
- mock.onPost(state.createProjectEndpoint).reply(statusCodes.ACCEPTED, {
+ mock.onPost(state.createProjectEndpoint).reply(HTTP_STATUS_ACCEPTED, {
job_id: '123',
});
mock.onGet(state.createProjectStatusEndpoint).reply(statusCodes.OK, {
@@ -151,7 +151,7 @@ describe('self-monitor actions', () => {
beforeEach(() => {
state.deleteProjectEndpoint = '/delete';
state.deleteProjectStatusEndpoint = '/delete-status';
- mock.onDelete(state.deleteProjectEndpoint).reply(statusCodes.ACCEPTED, {
+ mock.onDelete(state.deleteProjectEndpoint).reply(HTTP_STATUS_ACCEPTED, {
job_id: '456',
});
mock.onGet(state.deleteProjectStatusEndpoint).reply(statusCodes.OK, {
diff --git a/spec/frontend/sentry/index_spec.js b/spec/frontend/sentry/index_spec.js
index d1f098112e8..2dd528a8a1c 100644
--- a/spec/frontend/sentry/index_spec.js
+++ b/spec/frontend/sentry/index_spec.js
@@ -1,17 +1,20 @@
import index from '~/sentry/index';
+
+import LegacySentryConfig from '~/sentry/legacy_sentry_config';
import SentryConfig from '~/sentry/sentry_config';
-describe('SentryConfig options', () => {
+describe('Sentry init', () => {
+ let originalGon;
+
const dsn = 'https://123@sentry.gitlab.test/123';
- const currentUserId = 'currentUserId';
- const gitlabUrl = 'gitlabUrl';
const environment = 'test';
+ const currentUserId = '1';
+ const gitlabUrl = 'gitlabUrl';
const revision = 'revision';
const featureCategory = 'my_feature_category';
- let indexReturnValue;
-
beforeEach(() => {
+ originalGon = window.gon;
window.gon = {
sentry_dsn: dsn,
sentry_environment: environment,
@@ -21,28 +24,41 @@ describe('SentryConfig options', () => {
feature_category: featureCategory,
};
- process.env.HEAD_COMMIT_SHA = revision;
-
+ jest.spyOn(LegacySentryConfig, 'init').mockImplementation();
jest.spyOn(SentryConfig, 'init').mockImplementation();
+ });
- indexReturnValue = index();
+ afterEach(() => {
+ window.gon = originalGon;
});
- it('should init with .sentryDsn, .currentUserId, .whitelistUrls and environment', () => {
- expect(SentryConfig.init).toHaveBeenCalledWith({
- dsn,
- currentUserId,
- whitelistUrls: [gitlabUrl, 'webpack-internal://'],
- environment,
- release: revision,
- tags: {
- revision,
- feature_category: featureCategory,
- },
- });
+ it('exports new version of Sentry in the global object', () => {
+ // eslint-disable-next-line no-underscore-dangle
+ expect(window._Sentry.SDK_VERSION).not.toMatch(/^5\./);
});
- it('should return SentryConfig', () => {
- expect(indexReturnValue).toBe(SentryConfig);
+ describe('when called', () => {
+ beforeEach(() => {
+ index();
+ });
+
+ it('configures sentry', () => {
+ expect(SentryConfig.init).toHaveBeenCalledTimes(1);
+ expect(SentryConfig.init).toHaveBeenCalledWith({
+ dsn,
+ currentUserId,
+ allowUrls: [gitlabUrl, 'webpack-internal://'],
+ environment,
+ release: revision,
+ tags: {
+ revision,
+ feature_category: featureCategory,
+ },
+ });
+ });
+
+ it('does not configure legacy sentry', () => {
+ expect(LegacySentryConfig.init).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/sentry/legacy_index_spec.js b/spec/frontend/sentry/legacy_index_spec.js
new file mode 100644
index 00000000000..5c336f8392e
--- /dev/null
+++ b/spec/frontend/sentry/legacy_index_spec.js
@@ -0,0 +1,64 @@
+import index from '~/sentry/legacy_index';
+
+import LegacySentryConfig from '~/sentry/legacy_sentry_config';
+import SentryConfig from '~/sentry/sentry_config';
+
+describe('Sentry init', () => {
+ let originalGon;
+
+ const dsn = 'https://123@sentry.gitlab.test/123';
+ const environment = 'test';
+ const currentUserId = '1';
+ const gitlabUrl = 'gitlabUrl';
+ const revision = 'revision';
+ const featureCategory = 'my_feature_category';
+
+ beforeEach(() => {
+ originalGon = window.gon;
+ window.gon = {
+ sentry_dsn: dsn,
+ sentry_environment: environment,
+ current_user_id: currentUserId,
+ gitlab_url: gitlabUrl,
+ revision,
+ feature_category: featureCategory,
+ };
+
+ jest.spyOn(LegacySentryConfig, 'init').mockImplementation();
+ jest.spyOn(SentryConfig, 'init').mockImplementation();
+ });
+
+ afterEach(() => {
+ window.gon = originalGon;
+ });
+
+ it('exports legacy version of Sentry in the global object', () => {
+ // eslint-disable-next-line no-underscore-dangle
+ expect(window._Sentry.SDK_VERSION).toMatch(/^5\./);
+ });
+
+ describe('when called', () => {
+ beforeEach(() => {
+ index();
+ });
+
+ it('configures legacy sentry', () => {
+ expect(LegacySentryConfig.init).toHaveBeenCalledTimes(1);
+ expect(LegacySentryConfig.init).toHaveBeenCalledWith({
+ dsn,
+ currentUserId,
+ whitelistUrls: [gitlabUrl, 'webpack-internal://'],
+ environment,
+ release: revision,
+ tags: {
+ revision,
+ feature_category: featureCategory,
+ },
+ });
+ });
+
+ it('does not configure new sentry', () => {
+ expect(SentryConfig.init).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/sentry/legacy_sentry_config_spec.js b/spec/frontend/sentry/legacy_sentry_config_spec.js
new file mode 100644
index 00000000000..fe90cb49074
--- /dev/null
+++ b/spec/frontend/sentry/legacy_sentry_config_spec.js
@@ -0,0 +1,215 @@
+import * as Sentry5 from 'sentrybrowser5';
+import LegacySentryConfig from '~/sentry/legacy_sentry_config';
+
+describe('LegacySentryConfig', () => {
+ describe('IGNORE_ERRORS', () => {
+ it('should be an array of strings', () => {
+ const areStrings = LegacySentryConfig.IGNORE_ERRORS.every(
+ (error) => typeof error === 'string',
+ );
+
+ expect(areStrings).toBe(true);
+ });
+ });
+
+ describe('BLACKLIST_URLS', () => {
+ it('should be an array of regexps', () => {
+ const areRegExps = LegacySentryConfig.BLACKLIST_URLS.every((url) => url instanceof RegExp);
+
+ expect(areRegExps).toBe(true);
+ });
+ });
+
+ describe('SAMPLE_RATE', () => {
+ it('should be a finite number', () => {
+ expect(typeof LegacySentryConfig.SAMPLE_RATE).toEqual('number');
+ });
+ });
+
+ describe('init', () => {
+ const options = {
+ currentUserId: 1,
+ };
+
+ beforeEach(() => {
+ jest.spyOn(LegacySentryConfig, 'configure');
+ jest.spyOn(LegacySentryConfig, 'bindSentryErrors');
+ jest.spyOn(LegacySentryConfig, 'setUser');
+
+ LegacySentryConfig.init(options);
+ });
+
+ it('should set the options property', () => {
+ expect(LegacySentryConfig.options).toEqual(options);
+ });
+
+ it('should call the configure method', () => {
+ expect(LegacySentryConfig.configure).toHaveBeenCalled();
+ });
+
+ it('should call the error bindings method', () => {
+ expect(LegacySentryConfig.bindSentryErrors).toHaveBeenCalled();
+ });
+
+ it('should call setUser', () => {
+ expect(LegacySentryConfig.setUser).toHaveBeenCalled();
+ });
+
+ it('should not call setUser if there is no current user ID', () => {
+ LegacySentryConfig.setUser.mockClear();
+ options.currentUserId = undefined;
+
+ LegacySentryConfig.init(options);
+
+ expect(LegacySentryConfig.setUser).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('configure', () => {
+ const sentryConfig = {};
+ const options = {
+ dsn: 'https://123@sentry.gitlab.test/123',
+ whitelistUrls: ['//gitlabUrl', 'webpack-internal://'],
+ environment: 'test',
+ release: 'revision',
+ tags: {
+ revision: 'revision',
+ feature_category: 'my_feature_category',
+ },
+ };
+
+ beforeEach(() => {
+ jest.spyOn(Sentry5, 'init').mockImplementation();
+ jest.spyOn(Sentry5, 'setTags').mockImplementation();
+
+ sentryConfig.options = options;
+ sentryConfig.IGNORE_ERRORS = 'ignore_errors';
+ sentryConfig.BLACKLIST_URLS = 'blacklist_urls';
+
+ LegacySentryConfig.configure.call(sentryConfig);
+ });
+
+ it('should call Sentry5.init', () => {
+ expect(Sentry5.init).toHaveBeenCalledWith({
+ dsn: options.dsn,
+ release: options.release,
+ sampleRate: 0.95,
+ whitelistUrls: options.whitelistUrls,
+ environment: 'test',
+ ignoreErrors: sentryConfig.IGNORE_ERRORS,
+ blacklistUrls: sentryConfig.BLACKLIST_URLS,
+ });
+ });
+
+ it('should call Sentry5.setTags', () => {
+ expect(Sentry5.setTags).toHaveBeenCalledWith(options.tags);
+ });
+
+ it('should set environment from options', () => {
+ sentryConfig.options.environment = 'development';
+
+ LegacySentryConfig.configure.call(sentryConfig);
+
+ expect(Sentry5.init).toHaveBeenCalledWith({
+ dsn: options.dsn,
+ release: options.release,
+ sampleRate: 0.95,
+ whitelistUrls: options.whitelistUrls,
+ environment: 'development',
+ ignoreErrors: sentryConfig.IGNORE_ERRORS,
+ blacklistUrls: sentryConfig.BLACKLIST_URLS,
+ });
+ });
+ });
+
+ describe('setUser', () => {
+ let sentryConfig;
+
+ beforeEach(() => {
+ sentryConfig = { options: { currentUserId: 1 } };
+ jest.spyOn(Sentry5, 'setUser');
+
+ LegacySentryConfig.setUser.call(sentryConfig);
+ });
+
+ it('should call .setUser', () => {
+ expect(Sentry5.setUser).toHaveBeenCalledWith({
+ id: sentryConfig.options.currentUserId,
+ });
+ });
+ });
+
+ describe('handleSentryErrors', () => {
+ let event;
+ let req;
+ let config;
+ let err;
+
+ beforeEach(() => {
+ event = {};
+ req = { status: 'status', responseText: 'Unknown response text', statusText: 'statusText' };
+ config = { type: 'type', url: 'url', data: 'data' };
+ err = {};
+
+ jest.spyOn(Sentry5, 'captureMessage');
+
+ LegacySentryConfig.handleSentryErrors(event, req, config, err);
+ });
+
+ it('should call Sentry5.captureMessage', () => {
+ expect(Sentry5.captureMessage).toHaveBeenCalledWith(err, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: req.responseText,
+ error: err,
+ event,
+ },
+ });
+ });
+
+ describe('if no err is provided', () => {
+ beforeEach(() => {
+ LegacySentryConfig.handleSentryErrors(event, req, config);
+ });
+
+ it('should use req.statusText as the error value', () => {
+ expect(Sentry5.captureMessage).toHaveBeenCalledWith(req.statusText, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: req.responseText,
+ error: req.statusText,
+ event,
+ },
+ });
+ });
+ });
+
+ describe('if no req.responseText is provided', () => {
+ beforeEach(() => {
+ req.responseText = undefined;
+
+ LegacySentryConfig.handleSentryErrors(event, req, config, err);
+ });
+
+ it('should use `Unknown response text` as the response', () => {
+ expect(Sentry5.captureMessage).toHaveBeenCalledWith(err, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: 'Unknown response text',
+ error: err,
+ event,
+ },
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sentry/sentry_browser_wrapper_spec.js b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
new file mode 100644
index 00000000000..f4d646bab78
--- /dev/null
+++ b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
@@ -0,0 +1,59 @@
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+
+const mockError = new Error('error!');
+const mockMsg = 'msg!';
+const mockFn = () => {};
+
+describe('SentryBrowserWrapper', () => {
+ afterEach(() => {
+ // eslint-disable-next-line no-underscore-dangle
+ delete window._Sentry;
+ });
+
+ describe('when _Sentry is not defined', () => {
+ it('methods fail silently', () => {
+ expect(() => {
+ Sentry.captureException(mockError);
+ Sentry.captureMessage(mockMsg);
+ Sentry.withScope(mockFn);
+ }).not.toThrow();
+ });
+ });
+
+ describe('when _Sentry is defined', () => {
+ let mockCaptureException;
+ let mockCaptureMessage;
+ let mockWithScope;
+
+ beforeEach(async () => {
+ mockCaptureException = jest.fn();
+ mockCaptureMessage = jest.fn();
+ mockWithScope = jest.fn();
+
+ // eslint-disable-next-line no-underscore-dangle
+ window._Sentry = {
+ captureException: mockCaptureException,
+ captureMessage: mockCaptureMessage,
+ withScope: mockWithScope,
+ };
+ });
+
+ it('captureException is called', () => {
+ Sentry.captureException(mockError);
+
+ expect(mockCaptureException).toHaveBeenCalledWith(mockError);
+ });
+
+ it('captureMessage is called', () => {
+ Sentry.captureMessage(mockMsg);
+
+ expect(mockCaptureMessage).toHaveBeenCalledWith(mockMsg);
+ });
+
+ it('withScope is called', () => {
+ Sentry.withScope(mockFn);
+
+ expect(mockWithScope).toHaveBeenCalledWith(mockFn);
+ });
+ });
+});
diff --git a/spec/frontend/sentry/sentry_config_spec.js b/spec/frontend/sentry/sentry_config_spec.js
index 9f67b681b8d..44acbee9b38 100644
--- a/spec/frontend/sentry/sentry_config_spec.js
+++ b/spec/frontend/sentry/sentry_config_spec.js
@@ -1,29 +1,9 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from 'sentrybrowser7';
+import { IGNORE_ERRORS, DENY_URLS, SAMPLE_RATE } from '~/sentry/constants';
+
import SentryConfig from '~/sentry/sentry_config';
describe('SentryConfig', () => {
- describe('IGNORE_ERRORS', () => {
- it('should be an array of strings', () => {
- const areStrings = SentryConfig.IGNORE_ERRORS.every((error) => typeof error === 'string');
-
- expect(areStrings).toBe(true);
- });
- });
-
- describe('BLACKLIST_URLS', () => {
- it('should be an array of regexps', () => {
- const areRegExps = SentryConfig.BLACKLIST_URLS.every((url) => url instanceof RegExp);
-
- expect(areRegExps).toBe(true);
- });
- });
-
- describe('SAMPLE_RATE', () => {
- it('should be a finite number', () => {
- expect(typeof SentryConfig.SAMPLE_RATE).toEqual('number');
- });
- });
-
describe('init', () => {
const options = {
currentUserId: 1,
@@ -31,7 +11,6 @@ describe('SentryConfig', () => {
beforeEach(() => {
jest.spyOn(SentryConfig, 'configure');
- jest.spyOn(SentryConfig, 'bindSentryErrors');
jest.spyOn(SentryConfig, 'setUser');
SentryConfig.init(options);
@@ -45,19 +24,13 @@ describe('SentryConfig', () => {
expect(SentryConfig.configure).toHaveBeenCalled();
});
- it('should call the error bindings method', () => {
- expect(SentryConfig.bindSentryErrors).toHaveBeenCalled();
- });
-
it('should call setUser', () => {
expect(SentryConfig.setUser).toHaveBeenCalled();
});
it('should not call setUser if there is no current user ID', () => {
SentryConfig.setUser.mockClear();
- options.currentUserId = undefined;
-
- SentryConfig.init(options);
+ SentryConfig.init({ currentUserId: undefined });
expect(SentryConfig.setUser).not.toHaveBeenCalled();
});
@@ -67,7 +40,7 @@ describe('SentryConfig', () => {
const sentryConfig = {};
const options = {
dsn: 'https://123@sentry.gitlab.test/123',
- whitelistUrls: ['//gitlabUrl', 'webpack-internal://'],
+ allowUrls: ['//gitlabUrl', 'webpack-internal://'],
environment: 'test',
release: 'revision',
tags: {
@@ -81,8 +54,6 @@ describe('SentryConfig', () => {
jest.spyOn(Sentry, 'setTags').mockImplementation();
sentryConfig.options = options;
- sentryConfig.IGNORE_ERRORS = 'ignore_errors';
- sentryConfig.BLACKLIST_URLS = 'blacklist_urls';
SentryConfig.configure.call(sentryConfig);
});
@@ -91,11 +62,11 @@ describe('SentryConfig', () => {
expect(Sentry.init).toHaveBeenCalledWith({
dsn: options.dsn,
release: options.release,
- sampleRate: 0.95,
- whitelistUrls: options.whitelistUrls,
- environment: 'test',
- ignoreErrors: sentryConfig.IGNORE_ERRORS,
- blacklistUrls: sentryConfig.BLACKLIST_URLS,
+ sampleRate: SAMPLE_RATE,
+ allowUrls: options.allowUrls,
+ environment: options.environment,
+ ignoreErrors: IGNORE_ERRORS,
+ denyUrls: DENY_URLS,
});
});
@@ -111,11 +82,11 @@ describe('SentryConfig', () => {
expect(Sentry.init).toHaveBeenCalledWith({
dsn: options.dsn,
release: options.release,
- sampleRate: 0.95,
- whitelistUrls: options.whitelistUrls,
+ sampleRate: SAMPLE_RATE,
+ allowUrls: options.allowUrls,
environment: 'development',
- ignoreErrors: sentryConfig.IGNORE_ERRORS,
- blacklistUrls: sentryConfig.BLACKLIST_URLS,
+ ignoreErrors: IGNORE_ERRORS,
+ denyUrls: DENY_URLS,
});
});
});
@@ -136,78 +107,4 @@ describe('SentryConfig', () => {
});
});
});
-
- describe('handleSentryErrors', () => {
- let event;
- let req;
- let config;
- let err;
-
- beforeEach(() => {
- event = {};
- req = { status: 'status', responseText: 'Unknown response text', statusText: 'statusText' };
- config = { type: 'type', url: 'url', data: 'data' };
- err = {};
-
- jest.spyOn(Sentry, 'captureMessage');
-
- SentryConfig.handleSentryErrors(event, req, config, err);
- });
-
- it('should call Sentry.captureMessage', () => {
- expect(Sentry.captureMessage).toHaveBeenCalledWith(err, {
- extra: {
- type: config.type,
- url: config.url,
- data: config.data,
- status: req.status,
- response: req.responseText,
- error: err,
- event,
- },
- });
- });
-
- describe('if no err is provided', () => {
- beforeEach(() => {
- SentryConfig.handleSentryErrors(event, req, config);
- });
-
- it('should use req.statusText as the error value', () => {
- expect(Sentry.captureMessage).toHaveBeenCalledWith(req.statusText, {
- extra: {
- type: config.type,
- url: config.url,
- data: config.data,
- status: req.status,
- response: req.responseText,
- error: req.statusText,
- event,
- },
- });
- });
- });
-
- describe('if no req.responseText is provided', () => {
- beforeEach(() => {
- req.responseText = undefined;
-
- SentryConfig.handleSentryErrors(event, req, config, err);
- });
-
- it('should use `Unknown response text` as the response', () => {
- expect(Sentry.captureMessage).toHaveBeenCalledWith(err, {
- extra: {
- type: config.type,
- url: config.url,
- data: config.data,
- status: req.status,
- response: 'Unknown response text',
- error: err,
- event,
- },
- });
- });
- });
- });
});
diff --git a/spec/frontend/sidebar/assignee_title_spec.js b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
index 14a6bdbf907..14a6bdbf907 100644
--- a/spec/frontend/sidebar/assignee_title_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
diff --git a/spec/frontend/sidebar/assignees_realtime_spec.js b/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js
index ae8f07bf901..080171fb2ea 100644
--- a/spec/frontend/sidebar/assignees_realtime_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js
@@ -6,12 +6,12 @@ import waitForPromises from 'helpers/wait_for_promises';
import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
import SidebarMediator from '~/sidebar/sidebar_mediator';
-import getIssueAssigneesQuery from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
+import getIssueAssigneesQuery from '~/sidebar/queries/get_issue_assignees.query.graphql';
import Mock, {
issuableQueryResponse,
subscriptionNullResponse,
subscriptionResponse,
-} from './mock_data';
+} from '../../mock_data';
Vue.use(VueApollo);
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/components/assignees/assignees_spec.js
index 7cf7fd33022..6971ae2f9ed 100644
--- a/spec/frontend/sidebar/assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignees_spec.js
@@ -5,7 +5,7 @@ import { trimText } from 'helpers/text_helper';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import Assignee from '~/sidebar/components/assignees/assignees.vue';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
-import UsersMock from './mock_data';
+import UsersMock from '../../mock_data';
describe('Assignee component', () => {
const getDefaultProps = () => ({
diff --git a/spec/frontend/sidebar/issuable_assignees_spec.js b/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js
index 1161fefcc64..1161fefcc64 100644
--- a/spec/frontend/sidebar/issuable_assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js
diff --git a/spec/frontend/sidebar/sidebar_assignees_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
index 2cb2425532b..58b174059fa 100644
--- a/spec/frontend/sidebar/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
@@ -8,7 +8,7 @@ import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.v
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
-import Mock from './mock_data';
+import Mock from '../../mock_data';
describe('sidebar assignees', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
index cbb4c41dd14..3aca346ff5f 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -12,8 +12,8 @@ import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarInviteMembers from '~/sidebar/components/assignees/sidebar_invite_members.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import getIssueAssigneesQuery from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
-import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
+import getIssueAssigneesQuery from '~/sidebar/queries/get_issue_assignees.query.graphql';
+import updateIssueAssigneesMutation from '~/sidebar/queries/update_issue_assignees.mutation.graphql';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import { issuableQueryResponse, updateIssueAssigneesMutationResponse } from '../../mock_data';
diff --git a/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js b/spec/frontend/sidebar/components/copy/copy_email_to_clipboard_spec.js
index 69a8d645973..5b6db43a366 100644
--- a/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js
+++ b/spec/frontend/sidebar/components/copy/copy_email_to_clipboard_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import CopyEmailToClipboard from '~/sidebar/components/copy_email_to_clipboard.vue';
-import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
+import CopyEmailToClipboard from '~/sidebar/components/copy/copy_email_to_clipboard.vue';
+import CopyableField from '~/sidebar/components/copy/copyable_field.vue';
describe('CopyEmailToClipboard component', () => {
const mockIssueEmailAddress = 'sample+email@test.com';
diff --git a/spec/frontend/vue_shared/components/sidebar/copyable_field_spec.js b/spec/frontend/sidebar/components/copy/copyable_field_spec.js
index 3980033862e..7790d77bc65 100644
--- a/spec/frontend/vue_shared/components/sidebar/copyable_field_spec.js
+++ b/spec/frontend/sidebar/components/copy/copyable_field_spec.js
@@ -1,7 +1,7 @@
import { GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
+import CopyableField from '~/sidebar/components/copy/copyable_field.vue';
describe('SidebarCopyableField', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
index 69e35cd1d05..c5161a748a9 100644
--- a/spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js
+++ b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
@@ -4,10 +4,10 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { IssuableType } from '~/issues/constants';
-import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
+import SidebarReferenceWidget from '~/sidebar/components/copy/sidebar_reference_widget.vue';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
-import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
+import CopyableField from '~/sidebar/components/copy/copyable_field.vue';
import { issueReferenceResponse } from '../../mock_data';
describe('Sidebar Reference Widget', () => {
diff --git a/spec/frontend/sidebar/components/crm_contacts_spec.js b/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js
index 6d76fa1f9df..ca43c219d92 100644
--- a/spec/frontend/sidebar/components/crm_contacts_spec.js
+++ b/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js
@@ -5,13 +5,13 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import CrmContacts from '~/sidebar/components/crm_contacts/crm_contacts.vue';
-import getIssueCrmContactsQuery from '~/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql';
-import issueCrmContactsSubscription from '~/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql';
+import getIssueCrmContactsQuery from '~/sidebar/queries/get_issue_crm_contacts.query.graphql';
+import issueCrmContactsSubscription from '~/sidebar/queries/issue_crm_contacts.subscription.graphql';
import {
getIssueCrmContactsQueryResponse,
issueCrmContactsUpdateResponse,
issueCrmContactsUpdateNullResponse,
-} from './mock_data';
+} from '../mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
index 83764cb6739..1a78ce4ddee 100644
--- a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
+++ b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
@@ -3,11 +3,7 @@ import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import EscalationStatus from '~/sidebar/components/incidents/escalation_status.vue';
-import {
- STATUS_LABELS,
- STATUS_TRIGGERED,
- STATUS_ACKNOWLEDGED,
-} from '~/sidebar/components/incidents/constants';
+import { STATUS_LABELS, STATUS_TRIGGERED, STATUS_ACKNOWLEDGED } from '~/sidebar/constants';
describe('EscalationStatus', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/components/incidents/escalation_utils_spec.js b/spec/frontend/sidebar/components/incidents/escalation_utils_spec.js
index edd65db0325..d9e7f29c10e 100644
--- a/spec/frontend/sidebar/components/incidents/escalation_utils_spec.js
+++ b/spec/frontend/sidebar/components/incidents/escalation_utils_spec.js
@@ -1,5 +1,5 @@
-import { STATUS_ACKNOWLEDGED } from '~/sidebar/components/incidents/constants';
-import { getStatusLabel } from '~/sidebar/components/incidents/utils';
+import { STATUS_ACKNOWLEDGED } from '~/sidebar/constants';
+import { getStatusLabel } from '~/sidebar/utils';
describe('EscalationUtils', () => {
describe('getStatusLabel', () => {
diff --git a/spec/frontend/sidebar/components/incidents/mock_data.js b/spec/frontend/sidebar/components/incidents/mock_data.js
index bbb6c61b162..2a5b7798110 100644
--- a/spec/frontend/sidebar/components/incidents/mock_data.js
+++ b/spec/frontend/sidebar/components/incidents/mock_data.js
@@ -1,4 +1,4 @@
-import { STATUS_TRIGGERED, STATUS_ACKNOWLEDGED } from '~/sidebar/components/incidents/constants';
+import { STATUS_TRIGGERED, STATUS_ACKNOWLEDGED } from '~/sidebar/constants';
export const fetchData = {
workspace: {
diff --git a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
index 88a4913a27f..2dded61c073 100644
--- a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
+++ b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
@@ -1,5 +1,4 @@
-import { createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import {
fetchData,
@@ -12,26 +11,28 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import SidebarEscalationStatus from '~/sidebar/components/incidents/sidebar_escalation_status.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { escalationStatusQuery, escalationStatusMutation } from '~/sidebar/constants';
+import {
+ escalationStatusQuery,
+ escalationStatusMutation,
+ STATUS_ACKNOWLEDGED,
+} from '~/sidebar/constants';
import waitForPromises from 'helpers/wait_for_promises';
import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation_status.vue';
-import { STATUS_ACKNOWLEDGED } from '~/sidebar/components/incidents/constants';
import { createAlert } from '~/flash';
import { logError } from '~/lib/logger';
jest.mock('~/lib/logger');
jest.mock('~/flash');
-const localVue = createLocalVue();
+Vue.use(VueApollo);
describe('SidebarEscalationStatus', () => {
let wrapper;
+ let mockApollo;
const queryResolverMock = jest.fn();
const mutationResolverMock = jest.fn();
function createMockApolloProvider({ hasFetchError = false, hasMutationError = false } = {}) {
- localVue.use(VueApollo);
-
queryResolverMock.mockResolvedValue({ data: hasFetchError ? fetchError : fetchData });
mutationResolverMock.mockResolvedValue({
data: hasMutationError ? mutationError : mutationData,
@@ -45,15 +46,7 @@ describe('SidebarEscalationStatus', () => {
return createMockApollo(requestHandlers);
}
- function createComponent({ mockApollo } = {}) {
- let config;
-
- if (mockApollo) {
- config = { apolloProvider: mockApollo };
- } else {
- config = { mocks: { $apollo: { queries: { status: { loading: false } } } } };
- }
-
+ function createComponent(apolloProvider) {
wrapper = mountExtended(SidebarEscalationStatus, {
propsData: {
iid: '1',
@@ -66,13 +59,15 @@ describe('SidebarEscalationStatus', () => {
directives: {
GlTooltip: createMockDirective(),
},
- localVue,
- ...config,
+ apolloProvider,
});
+
+ // wait for apollo requests
+ return waitForPromises();
}
- afterEach(() => {
- wrapper.destroy();
+ beforeEach(() => {
+ mockApollo = createMockApolloProvider();
});
const findSidebarComponent = () => wrapper.findComponent(SidebarEditableItem);
@@ -80,36 +75,32 @@ describe('SidebarEscalationStatus', () => {
const findEditButton = () => wrapper.findByTestId('edit-button');
const findIcon = () => wrapper.findByTestId('status-icon');
- const clickEditButton = async () => {
+ const clickEditButton = () => {
findEditButton().vm.$emit('click');
- await nextTick();
+ return nextTick();
};
- const selectAcknowledgedStatus = async () => {
+ const selectAcknowledgedStatus = () => {
findStatusComponent().vm.$emit('input', STATUS_ACKNOWLEDGED);
// wait for apollo requests
- await waitForPromises();
+ return waitForPromises();
};
describe('sidebar', () => {
- it('renders the sidebar component', () => {
- createComponent();
+ it('renders the sidebar component', async () => {
+ await createComponent(mockApollo);
expect(findSidebarComponent().exists()).toBe(true);
});
describe('status icon', () => {
- it('is visible', () => {
- createComponent();
+ it('is visible', async () => {
+ await createComponent(mockApollo);
expect(findIcon().exists()).toBe(true);
expect(findIcon().isVisible()).toBe(true);
});
it('has correct tooltip', async () => {
- const mockApollo = createMockApolloProvider();
- createComponent({ mockApollo });
-
- // wait for apollo requests
- await waitForPromises();
+ await createComponent(mockApollo);
const tooltip = getBinding(findIcon().element, 'gl-tooltip');
@@ -120,11 +111,7 @@ describe('SidebarEscalationStatus', () => {
describe('status dropdown', () => {
beforeEach(async () => {
- const mockApollo = createMockApolloProvider();
- createComponent({ mockApollo });
-
- // wait for apollo requests
- await waitForPromises();
+ await createComponent(mockApollo);
});
it('is closed by default', () => {
@@ -148,11 +135,7 @@ describe('SidebarEscalationStatus', () => {
describe('update Status event', () => {
beforeEach(async () => {
- const mockApollo = createMockApolloProvider();
- createComponent({ mockApollo });
-
- // wait for apollo requests
- await waitForPromises();
+ await createComponent(mockApollo);
await clickEditButton();
await selectAcknowledgedStatus();
@@ -184,22 +167,16 @@ describe('SidebarEscalationStatus', () => {
describe('mutation errors', () => {
it('should error upon fetch', async () => {
- const mockApollo = createMockApolloProvider({ hasFetchError: true });
- createComponent({ mockApollo });
-
- // wait for apollo requests
- await waitForPromises();
+ mockApollo = createMockApolloProvider({ hasFetchError: true });
+ await createComponent(mockApollo);
expect(createAlert).toHaveBeenCalled();
expect(logError).toHaveBeenCalled();
});
it('should error upon mutation', async () => {
- const mockApollo = createMockApolloProvider({ hasMutationError: true });
- createComponent({ mockApollo });
-
- // wait for apollo requests
- await waitForPromises();
+ mockApollo = createMockApolloProvider({ hasMutationError: true });
+ await createComponent(mockApollo);
await clickEditButton();
await selectAcknowledgedStatus();
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js
index c0e5408e1bd..4f2a89e20db 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js
@@ -3,9 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
+import DropdownButton from '~/sidebar/components/labels/labels_select_vue/dropdown_button.vue';
-import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import labelSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
import { mockConfig } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
index 799e2c1d08e..59e95edfa20 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -3,9 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue';
+import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue';
-import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import labelSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
import { mockConfig, mockSuggestedColors } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js
index cc9b9f393ce..865dc8fe8fb 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -9,13 +9,13 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
-import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
-import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
+import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue';
+import LabelItem from '~/sidebar/components/labels/labels_select_vue/label_item.vue';
-import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
-import * as getters from '~/vue_shared/components/sidebar/labels_select_vue/store/getters';
-import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
-import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
+import * as actions from '~/sidebar/components/labels/labels_select_vue/store/actions';
+import * as getters from '~/sidebar/components/labels/labels_select_vue/store/getters';
+import mutations from '~/sidebar/components/labels/labels_select_vue/store/mutations';
+import defaultState from '~/sidebar/components/labels/labels_select_vue/store/state';
import { mockConfig, mockLabels, mockRegularLabel } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js
index 9781d9c4de0..e9ffda7c251 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js
@@ -2,9 +2,9 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
-import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import { DropdownVariant } from '~/sidebar/components/labels/labels_select_vue/constants';
+import DropdownContents from '~/sidebar/components/labels/labels_select_vue/dropdown_contents.vue';
+import labelsSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
import { mockConfig } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js
index 54804f85f81..6c3fda421ff 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js
@@ -3,9 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
+import DropdownTitle from '~/sidebar/components/labels/labels_select_vue/dropdown_title.vue';
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import labelsSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
import { mockConfig } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js
index c6400320dea..56f25a1c6a4 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import DropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
+import DropdownValueCollapsedComponent from '~/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed.vue';
import { mockCollapsedLabels as mockLabels, mockRegularLabel } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js
index f3c4839002b..a1ccc9d2ab1 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js
@@ -3,9 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_vue/dropdown_value.vue';
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import labelsSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
import { mockConfig, mockLabels, mockRegularLabel, mockScopedLabel } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js
index bb0f1777de6..e14c0e308ce 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js
@@ -1,7 +1,7 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
+import LabelItem from '~/sidebar/components/labels/labels_select_vue/label_item.vue';
import { mockRegularLabel } from './mock_data';
const mockLabel = { ...mockRegularLabel, set: true };
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js
index 30c1a4b7d2f..a3b10c18374 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js
@@ -3,15 +3,15 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
-import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
-import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
-import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
-import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
-import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
-import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
-
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import { DropdownVariant } from '~/sidebar/components/labels/labels_select_vue/constants';
+import DropdownButton from '~/sidebar/components/labels/labels_select_vue/dropdown_button.vue';
+import DropdownContents from '~/sidebar/components/labels/labels_select_vue/dropdown_contents.vue';
+import DropdownTitle from '~/sidebar/components/labels/labels_select_vue/dropdown_title.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_vue/dropdown_value.vue';
+import DropdownValueCollapsed from '~/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed.vue';
+import LabelsSelectRoot from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue';
+
+import labelsSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
import { mockConfig } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js b/spec/frontend/sidebar/components/labels/labels_select_vue/mock_data.js
index 884bc4684ba..884bc4684ba 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/mock_data.js
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
index edd044bd754..0e0024aa6c2 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
@@ -3,9 +3,9 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
-import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
-import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
+import * as actions from '~/sidebar/components/labels/labels_select_vue/store/actions';
+import * as types from '~/sidebar/components/labels/labels_select_vue/store/mutation_types';
+import defaultState from '~/sidebar/components/labels/labels_select_vue/store/state';
jest.mock('~/flash');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/store/getters_spec.js
index 6ad46dbe898..e32256831a3 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/store/getters_spec.js
@@ -1,4 +1,4 @@
-import * as getters from '~/vue_shared/components/sidebar/labels_select_vue/store/getters';
+import * as getters from '~/sidebar/components/labels/labels_select_vue/store/getters';
describe('LabelsSelect Getters', () => {
describe('dropdownButtonText', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/store/mutations_spec.js
index 2b2508b5e11..cee5d2e77d1 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/store/mutations_spec.js
@@ -1,6 +1,6 @@
import { cloneDeep } from 'lodash';
-import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
-import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
+import * as types from '~/sidebar/components/labels/labels_select_vue/store/mutation_types';
+import mutations from '~/sidebar/components/labels/labels_select_vue/store/mutations';
describe('LabelsSelect Mutations', () => {
describe(`${types.SET_INITIAL_STATE}`, () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
index 237f174e048..79b164b0ea7 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -6,8 +6,8 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { workspaceLabelsQueries } from '~/sidebar/constants';
-import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
-import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
+import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
+import createLabelMutation from '~/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql';
import {
mockRegularLabel,
mockSuggestedColors,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
index 5d8ad5ddee5..913badccbe4 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -11,10 +11,10 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
-import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
-import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
-import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
+import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants';
+import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue';
+import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { mockConfig, workspaceLabelsQueryResponse } from './mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
index 00da9b74957..9bbb1413ee9 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
-import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
-import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
-import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
-import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
+import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants';
+import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue';
+import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
+import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue';
+import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue';
import { mockLabels } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js
index 0508a059195..9a6e0ca3ccd 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
+import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue';
describe('DropdownFooter', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
index c4faef8ccdd..d9001dface4 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
@@ -1,7 +1,7 @@
import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
+import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue';
describe('DropdownHeader', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js
index 0c4f4b7d504..585048983c9 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js
@@ -1,7 +1,7 @@
import { GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
import { mockRegularLabel, mockScopedLabel } from './mock_data';
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js
new file mode 100644
index 00000000000..4fa65c752f9
--- /dev/null
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js
@@ -0,0 +1,77 @@
+import { GlLabel } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import EmbeddedLabelsList from '~/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue';
+import { mockRegularLabel, mockScopedLabel } from './mock_data';
+
+describe('EmbeddedLabelsList', () => {
+ let wrapper;
+
+ const findAllLabels = () => wrapper.findAllComponents(GlLabel);
+ const findLabelByTitle = (title) =>
+ findAllLabels()
+ .filter((label) => label.props('title') === title)
+ .at(0);
+ const findRegularLabel = () => findLabelByTitle(mockRegularLabel.title);
+ const findScopedLabel = () => findLabelByTitle(mockScopedLabel.title);
+
+ const createComponent = (props = {}, slots = {}) => {
+ wrapper = shallowMountExtended(EmbeddedLabelsList, {
+ slots,
+ propsData: {
+ selectedLabels: [mockRegularLabel, mockScopedLabel],
+ allowLabelRemove: true,
+ labelsFilterBasePath: '/gitlab-org/my-project/issues',
+ labelsFilterParam: 'label_name',
+ ...props,
+ },
+ provide: {
+ allowScopedLabels: true,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when there are no labels', () => {
+ beforeEach(() => {
+ createComponent({
+ selectedLabels: [],
+ });
+ });
+
+ it('does not render any labels', () => {
+ expect(findAllLabels()).toHaveLength(0);
+ });
+ });
+
+ describe('when there are labels', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders a list of two labels', () => {
+ expect(findAllLabels()).toHaveLength(2);
+ });
+
+ it('passes correct props to the regular label', () => {
+ expect(findRegularLabel().props('target')).toBe(
+ '/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
+ );
+ expect(findRegularLabel().props('scoped')).toBe(false);
+ });
+
+ it('passes correct props to the scoped label', () => {
+ expect(findScopedLabel().props('target')).toBe(
+ '/gitlab-org/my-project/issues?label_name[]=Foo%3A%3ABar',
+ );
+ expect(findScopedLabel().props('scoped')).toBe(true);
+ });
+
+ it('emits `onLabelRemove` event with the correct ID', () => {
+ findRegularLabel().vm.$emit('close');
+ expect(wrapper.emitted('onLabelRemove')).toStrictEqual([[mockRegularLabel.id]]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js
index 6e8841411a2..74188a77994 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
+import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { mockRegularLabel } from './mock_data';
const mockLabel = { ...mockRegularLabel, set: true };
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
index 74ddd07d041..2995c268966 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
@@ -6,19 +6,22 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
-import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
-import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
+import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
+import EmbeddedLabelsList from '~/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue';
+import issueLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/issue_labels.query.graphql';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labels.subscription.graphql';
-import updateEpicLabelsMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
-import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import updateEpicLabelsMutation from '~/sidebar/components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
+import LabelsSelectRoot from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
import {
mockConfig,
issuableLabelsQueryResponse,
updateLabelsMutationResponse,
issuableLabelsSubscriptionResponse,
+ mockLabels,
+ mockRegularLabel,
} from './mock_data';
jest.mock('~/flash');
@@ -42,6 +45,7 @@ describe('LabelsSelectRoot', () => {
const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findDropdownValue = () => wrapper.findComponent(DropdownValue);
const findDropdownContents = () => wrapper.findComponent(DropdownContents);
+ const findEmbeddedLabelsList = () => wrapper.findComponent(EmbeddedLabelsList);
const createComponent = ({
config = mockConfig,
@@ -151,6 +155,52 @@ describe('LabelsSelectRoot', () => {
});
});
+ describe('if dropdown variant is `embedded`', () => {
+ it('shows the embedded labels list', () => {
+ createComponent({
+ config: { ...mockConfig, iid: '', variant: 'embedded', showEmbeddedLabelsList: true },
+ });
+
+ expect(findEmbeddedLabelsList().props()).toMatchObject({
+ disabled: false,
+ selectedLabels: [],
+ allowLabelRemove: false,
+ labelsFilterBasePath: mockConfig.labelsFilterBasePath,
+ labelsFilterParam: mockConfig.labelsFilterParam,
+ });
+ });
+
+ it('passes the selected labels if provided', () => {
+ createComponent({
+ config: {
+ ...mockConfig,
+ iid: '',
+ variant: 'embedded',
+ showEmbeddedLabelsList: true,
+ selectedLabels: mockLabels,
+ },
+ });
+
+ expect(findEmbeddedLabelsList().props('selectedLabels')).toStrictEqual(mockLabels);
+ expect(findDropdownContents().props('selectedLabels')).toStrictEqual(mockLabels);
+ });
+
+ it('emits the `onLabelRemove` when the embedded list triggers a removal', () => {
+ createComponent({
+ config: {
+ ...mockConfig,
+ iid: '',
+ variant: 'embedded',
+ showEmbeddedLabelsList: true,
+ selectedLabels: [mockRegularLabel],
+ },
+ });
+
+ findEmbeddedLabelsList().vm.$emit('onLabelRemove', [mockRegularLabel.id]);
+ expect(wrapper.emitted('onLabelRemove')).toStrictEqual([[[mockRegularLabel.id]]]);
+ });
+ });
+
it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if iid is not set', async () => {
const label = { id: 'gid://gitlab/ProjectLabel/1' };
createComponent({ config: { ...mockConfig, iid: undefined } });
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
index 48530a0261f..48530a0261f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
diff --git a/spec/frontend/sidebar/lock/__snapshots__/edit_form_spec.js.snap b/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
index 18d4df297df..18d4df297df 100644
--- a/spec/frontend/sidebar/lock/__snapshots__/edit_form_spec.js.snap
+++ b/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
diff --git a/spec/frontend/sidebar/lock/constants.js b/spec/frontend/sidebar/components/lock/constants.js
index b9f08e9286d..b9f08e9286d 100644
--- a/spec/frontend/sidebar/lock/constants.js
+++ b/spec/frontend/sidebar/components/lock/constants.js
diff --git a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js
index 2abb0c24d7d..2abb0c24d7d 100644
--- a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
+++ b/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js
diff --git a/spec/frontend/sidebar/lock/edit_form_spec.js b/spec/frontend/sidebar/components/lock/edit_form_spec.js
index 4ae9025ee39..4ae9025ee39 100644
--- a/spec/frontend/sidebar/lock/edit_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/edit_form_spec.js
diff --git a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
index 8f825847cfc..8f825847cfc 100644
--- a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
index d531147c0e6..72279f44e80 100644
--- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
@@ -12,7 +12,7 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import axios from '~/lib/utils/axios_utils';
-import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
+import IssuableMoveDropdown from '~/sidebar/components/move/issuable_move_dropdown.vue';
const mockProjects = [
{
diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
index c432d722637..999340da27c 100644
--- a/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js
+++ b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
@@ -6,12 +6,12 @@ import { GlAlert } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { logError } from '~/lib/logger';
-import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
-import MoveIssuesButton from '~/issuable/bulk_update_sidebar/components/move_issues_button.vue';
+import IssuableMoveDropdown from '~/sidebar/components/move/issuable_move_dropdown.vue';
import issuableEventHub from '~/issues/list/eventhub';
-import moveIssueMutation from '~/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql';
+import MoveIssuesButton from '~/sidebar/components/move/move_issues_button.vue';
+import moveIssueMutation from '~/sidebar/queries/move_issue.mutation.graphql';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
import { getIssuesCountsQueryResponse, getIssuesQueryResponse } from 'jest/issues/list/mock_data';
@@ -389,7 +389,7 @@ describe('MoveIssuesButton', () => {
await waitForPromises();
expect(logError).not.toHaveBeenCalled();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('does not create flashes or logs errors when only tasks are selected', async () => {
@@ -399,7 +399,7 @@ describe('MoveIssuesButton', () => {
await waitForPromises();
expect(logError).not.toHaveBeenCalled();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('does not create flashes or logs errors when only test cases are selected', async () => {
@@ -409,7 +409,7 @@ describe('MoveIssuesButton', () => {
await waitForPromises();
expect(logError).not.toHaveBeenCalled();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('does not create flashes or logs errors when only tasks and test cases are selected', async () => {
@@ -419,7 +419,7 @@ describe('MoveIssuesButton', () => {
await waitForPromises();
expect(logError).not.toHaveBeenCalled();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('does not create flashes or logs errors when issues are moved without errors', async () => {
@@ -432,7 +432,7 @@ describe('MoveIssuesButton', () => {
await waitForPromises();
expect(logError).not.toHaveBeenCalled();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('creates a flash and logs errors when a mutation returns errors', async () => {
@@ -456,8 +456,8 @@ describe('MoveIssuesButton', () => {
);
// Only one flash is created even if multiple errors are reported
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was an error while moving the issues.',
});
});
@@ -469,8 +469,8 @@ describe('MoveIssuesButton', () => {
await waitForPromises();
expect(logError).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was an error while moving the issues.',
});
});
diff --git a/spec/frontend/sidebar/participants_spec.js b/spec/frontend/sidebar/components/participants/participants_spec.js
index f7a626a189c..f7a626a189c 100644
--- a/spec/frontend/sidebar/participants_spec.js
+++ b/spec/frontend/sidebar/components/participants/participants_spec.js
diff --git a/spec/frontend/sidebar/reviewer_title_spec.js b/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
index 68ecd62e4c6..68ecd62e4c6 100644
--- a/spec/frontend/sidebar/reviewer_title_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
diff --git a/spec/frontend/sidebar/reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/reviewers_spec.js
index 229f7ffbe04..229f7ffbe04 100644
--- a/spec/frontend/sidebar/reviewers_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/reviewers_spec.js
diff --git a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js
new file mode 100644
index 00000000000..57ae146a27a
--- /dev/null
+++ b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js
@@ -0,0 +1,77 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import axios from 'axios';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import SidebarReviewers from '~/sidebar/components/reviewers/sidebar_reviewers.vue';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import Mock from '../../mock_data';
+
+Vue.use(VueApollo);
+
+describe('sidebar reviewers', () => {
+ const apolloMock = createMockApollo();
+ let wrapper;
+ let mediator;
+ let axiosMock;
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(SidebarReviewers, {
+ apolloProvider: apolloMock,
+ propsData: {
+ issuableIid: '1',
+ issuableId: 1,
+ mediator,
+ field: '',
+ projectPath: 'projectPath',
+ changing: false,
+ ...props,
+ },
+ // Attaching to document is required because this component emits something from the parent element :/
+ attachTo: document.body,
+ });
+ };
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ mediator = new SidebarMediator(Mock.mediator);
+
+ jest.spyOn(mediator, 'saveReviewers');
+ jest.spyOn(mediator, 'addSelfReview');
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ axiosMock.restore();
+ });
+
+ it('calls the mediator when it saves the reviewers', () => {
+ createComponent();
+
+ expect(mediator.saveReviewers).not.toHaveBeenCalled();
+
+ wrapper.vm.saveReviewers();
+
+ expect(mediator.saveReviewers).toHaveBeenCalled();
+ });
+
+ it('calls the mediator when "reviewBySelf" method is called', () => {
+ createComponent();
+
+ expect(mediator.addSelfReview).not.toHaveBeenCalled();
+ expect(mediator.store.reviewers.length).toBe(0);
+
+ wrapper.vm.reviewBySelf();
+
+ expect(mediator.addSelfReview).toHaveBeenCalled();
+ expect(mediator.store.reviewers.length).toBe(1);
+ });
+});
diff --git a/spec/frontend/sidebar/components/severity/severity_spec.js b/spec/frontend/sidebar/components/severity/severity_spec.js
index 2146155791e..99d33e840d5 100644
--- a/spec/frontend/sidebar/components/severity/severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/severity_spec.js
@@ -1,6 +1,6 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
+import { INCIDENT_SEVERITY } from '~/sidebar/constants';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
describe('SeverityToken', () => {
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
index bdea33371d8..8f936240b7a 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
@@ -3,8 +3,8 @@ import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import { INCIDENT_SEVERITY, ISSUABLE_TYPES } from '~/sidebar/components/severity/constants';
-import updateIssuableSeverity from '~/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql';
+import { INCIDENT_SEVERITY, ISSUABLE_TYPES } from '~/sidebar/constants';
+import updateIssuableSeverity from '~/sidebar/queries/update_issuable_severity.mutation.graphql';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/status_dropdown_spec.js b/spec/frontend/sidebar/components/status/status_dropdown_spec.js
index 2f281cb88f9..5a75299c3a4 100644
--- a/spec/frontend/issuable/bulk_update_sidebar/components/status_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/status/status_dropdown_spec.js
@@ -1,7 +1,7 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import StatusDropdown from '~/issuable/bulk_update_sidebar/components/status_dropdown.vue';
-import { statusDropdownOptions } from '~/issuable/bulk_update_sidebar/constants';
+import StatusDropdown from '~/sidebar/components/status/status_dropdown.vue';
+import { statusDropdownOptions } from '~/sidebar/constants';
describe('SubscriptionsDropdown component', () => {
let wrapper;
diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/subscriptions_dropdown_spec.js b/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js
index 56ef7a1ed39..3fb8214606c 100644
--- a/spec/frontend/issuable/bulk_update_sidebar/components/subscriptions_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js
@@ -1,8 +1,8 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import SubscriptionsDropdown from '~/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue';
-import { subscriptionsDropdownOptions } from '~/issuable/bulk_update_sidebar/constants';
+import SubscriptionsDropdown from '~/sidebar/components/subscriptions/subscriptions_dropdown.vue';
+import { subscriptionsDropdownOptions } from '~/sidebar/constants';
describe('SubscriptionsDropdown component', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/subscriptions_spec.js b/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
index 1a1aa370eef..1a1aa370eef 100644
--- a/spec/frontend/sidebar/subscriptions_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
diff --git a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
new file mode 100644
index 00000000000..cb3bb7a4538
--- /dev/null
+++ b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
@@ -0,0 +1,219 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlAlert, GlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import CreateTimelogForm from '~/sidebar/components/time_tracking/create_timelog_form.vue';
+import createTimelogMutation from '~/sidebar/queries/create_timelog.mutation.graphql';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
+
+const mockMutationErrorMessage = 'Example error message';
+
+const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ timelogCreate: {
+ errors: [],
+ timelog: {
+ id: 'gid://gitlab/Timelog/1',
+ issue: {},
+ mergeRequest: {},
+ },
+ },
+ },
+});
+
+const resolvedMutationWithErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ timelogCreate: {
+ errors: [{ message: mockMutationErrorMessage }],
+ timelog: null,
+ },
+ },
+});
+
+const rejectedMutationMock = jest.fn().mockRejectedValue();
+const modalCloseMock = jest.fn();
+
+describe('Create Timelog Form', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ let fakeApollo;
+
+ const findForm = () => wrapper.find('form');
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findDocsLink = () => wrapper.findByTestId('timetracking-docs-link');
+ const findSaveButton = () => findModal().props('actionPrimary');
+ const findSaveButtonLoadingState = () => findSaveButton().attributes[0].loading;
+ const findSaveButtonDisabledState = () => findSaveButton().attributes[0].disabled;
+
+ const submitForm = () => findForm().trigger('submit');
+
+ const mountComponent = (
+ { props, data, providedProps } = {},
+ mutationResolverMock = rejectedMutationMock,
+ ) => {
+ fakeApollo = createMockApollo([[createTimelogMutation, mutationResolverMock]]);
+
+ wrapper = shallowMountExtended(CreateTimelogForm, {
+ data() {
+ return {
+ ...data,
+ };
+ },
+ provide: {
+ issuableType: 'issue',
+ ...providedProps,
+ },
+ propsData: {
+ issuableId: '1',
+ ...props,
+ },
+ apolloProvider: fakeApollo,
+ });
+
+ wrapper.vm.$refs.modal.close = modalCloseMock;
+ };
+
+ afterEach(() => {
+ fakeApollo = null;
+ });
+
+ describe('save button', () => {
+ it('is disabled and not loading by default', () => {
+ mountComponent();
+
+ expect(findSaveButtonLoadingState()).toBe(false);
+ expect(findSaveButtonDisabledState()).toBe(true);
+ });
+
+ it('is enabled and not loading when time spent is not empty', () => {
+ mountComponent({ data: { timeSpent: '2d' } });
+
+ expect(findSaveButtonLoadingState()).toBe(false);
+ expect(findSaveButtonDisabledState()).toBe(false);
+ });
+
+ it('is disabled and loading when the the form is submitted', async () => {
+ mountComponent({ data: { timeSpent: '2d' } });
+
+ submitForm();
+
+ await nextTick();
+
+ expect(findSaveButtonLoadingState()).toBe(true);
+ expect(findSaveButtonDisabledState()).toBe(true);
+ });
+
+ it('is enabled and not loading the when form is submitted but the mutation has errors', async () => {
+ mountComponent({ data: { timeSpent: '2d' } });
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(rejectedMutationMock).toHaveBeenCalled();
+ expect(findSaveButtonLoadingState()).toBe(false);
+ expect(findSaveButtonDisabledState()).toBe(false);
+ });
+
+ it('is enabled and not loading the when form is submitted but the mutation returns errors', async () => {
+ mountComponent({ data: { timeSpent: '2d' } }, resolvedMutationWithErrorsMock);
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithErrorsMock).toHaveBeenCalled();
+ expect(findSaveButtonLoadingState()).toBe(false);
+ expect(findSaveButtonDisabledState()).toBe(false);
+ });
+ });
+
+ describe('form', () => {
+ it('does not call any mutation when the the form is incomplete', async () => {
+ mountComponent();
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(rejectedMutationMock).not.toHaveBeenCalled();
+ });
+
+ it('closes the modal after a successful mutation', async () => {
+ mountComponent({ data: { timeSpent: '2d' } }, resolvedMutationWithoutErrorsMock);
+
+ submitForm();
+
+ await waitForPromises();
+ await nextTick();
+
+ expect(modalCloseMock).toHaveBeenCalled();
+ });
+
+ it.each`
+ issuableType | typeConstant
+ ${'issue'} | ${TYPE_ISSUE}
+ ${'merge_request'} | ${TYPE_MERGE_REQUEST}
+ `(
+ 'calls the mutation with all the fields when the the form is submitted and issuable type is $issuableType',
+ async ({ issuableType, typeConstant }) => {
+ const timeSpent = '2d';
+ const spentAt = '2022-11-20T21:53:00+0000';
+ const summary = 'Example';
+
+ mountComponent({ data: { timeSpent, spentAt, summary }, providedProps: { issuableType } });
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(rejectedMutationMock).toHaveBeenCalledWith({
+ input: { timeSpent, spentAt, summary, issuableId: convertToGraphQLId(typeConstant, '1') },
+ });
+ },
+ );
+ });
+
+ describe('alert', () => {
+ it('is hidden by default', () => {
+ mountComponent();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('shows an error if the submission fails with a handled error', async () => {
+ mountComponent({ data: { timeSpent: '2d' } }, resolvedMutationWithErrorsMock);
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(mockMutationErrorMessage);
+ });
+
+ it('shows an error if the submission fails with an unhandled error', async () => {
+ mountComponent({ data: { timeSpent: '2d' } });
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe('An error occurred while saving the time entry.');
+ });
+ });
+
+ describe('docs link message', () => {
+ it('is present', () => {
+ mountComponent();
+
+ expect(findDocsLink().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index af72122052f..0259aee48f0 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -8,9 +8,9 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import Report from '~/sidebar/components/time_tracking/report.vue';
-import getIssueTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql';
-import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql';
-import deleteTimelogMutation from '~/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql';
+import getIssueTimelogsQuery from '~/sidebar/queries/get_issue_timelogs.query.graphql';
+import getMrTimelogsQuery from '~/sidebar/queries/get_mr_timelogs.query.graphql';
+import deleteTimelogMutation from '~/sidebar/queries/delete_timelog.mutation.graphql';
import {
getIssueTimelogsQueryResponse,
getMrTimelogsQueryResponse,
diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
index 835e700e63c..45d8b5e4647 100644
--- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -268,47 +268,32 @@ describe('Issuable Time Tracker', () => {
});
});
- describe('Help pane', () => {
- const findHelpButton = () => findByTestId('helpButton');
- const findCloseHelpButton = () => findByTestId('closeHelpButton');
-
- beforeEach(async () => {
- wrapper = mountComponent({
- props: {
- initialTimeTracking: {
- timeEstimate: 0,
- totalTimeSpent: 0,
- humanTimeEstimate: '',
- humanTotalTimeSpent: '',
+ describe('Add button', () => {
+ const findAddButton = () => findByTestId('add-time-entry-button');
+
+ it.each`
+ visibility | canAddTimeEntries
+ ${'not visible'} | ${false}
+ ${'visible'} | ${true}
+ `(
+ 'is $visibility when canAddTimeEntries is $canAddTimeEntries',
+ async ({ canAddTimeEntries }) => {
+ wrapper = mountComponent({
+ props: {
+ initialTimeTracking: {
+ timeEstimate: 0,
+ totalTimeSpent: 0,
+ humanTimeEstimate: '',
+ humanTotalTimeSpent: '',
+ },
+ canAddTimeEntries,
},
- },
- });
- await nextTick();
- });
-
- it('should not show the "Help" pane by default', () => {
- expect(findByTestId('helpPane').exists()).toBe(false);
- });
-
- it('should show the "Help" pane when help button is clicked', async () => {
- findHelpButton().trigger('click');
-
- await nextTick();
-
- expect(findByTestId('helpPane').exists()).toBe(true);
- });
-
- it('should not show the "Help" pane when help button is clicked and then closed', async () => {
- findHelpButton().trigger('click');
- await nextTick();
-
- expect(findByTestId('helpPane').exists()).toBe(true);
-
- findCloseHelpButton().trigger('click');
- await nextTick();
+ });
+ await nextTick();
- expect(findByTestId('helpPane').exists()).toBe(false);
- });
+ expect(findAddButton().exists()).toBe(canAddTimeEntries);
+ },
+ );
});
});
diff --git a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap b/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
index 846f45345e7..846f45345e7 100644
--- a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
+++ b/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
diff --git a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
index f73491ca95f..5bfe3b59eb3 100644
--- a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
@@ -7,7 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
-import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
+import TodoButton from '~/sidebar/components/todo_toggle/todo_button.vue';
import { todosResponse, noTodosResponse } from '../../mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
index 01958a144ed..fb07029a249 100644
--- a/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
@@ -1,6 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
+import TodoButton from '~/sidebar/components/todo_toggle/todo_button.vue';
describe('Todo Button', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_spec.js
index 8e6597bf80f..8e6597bf80f 100644
--- a/spec/frontend/sidebar/todo_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/todo_spec.js
diff --git a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
index 267a467059d..cf9b2828dde 100644
--- a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
+++ b/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
@@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
+import ToggleSidebar from '~/sidebar/components/toggle/toggle_sidebar.vue';
describe('ToggleSidebar', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/sidebar_move_issue_spec.js b/spec/frontend/sidebar/lib/sidebar_move_issue_spec.js
index 195cc6ddeeb..6e365df329b 100644
--- a/spec/frontend/sidebar/sidebar_move_issue_spec.js
+++ b/spec/frontend/sidebar/lib/sidebar_move_issue_spec.js
@@ -8,7 +8,7 @@ import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import { GitLabDropdown } from '~/deprecated_jquery_dropdown/gl_dropdown';
-import Mock from './mock_data';
+import Mock from '../mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index bb5e7f7ff16..cdb9ced70b8 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -24,7 +24,8 @@ describe('Sidebar mediator', () => {
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
- mock.restore();
+
+ jest.clearAllMocks();
});
it('assigns yourself', () => {
@@ -42,6 +43,52 @@ describe('Sidebar mediator', () => {
});
});
+ it('assigns yourself as a reviewer', () => {
+ mediator.addSelfReview();
+
+ expect(mediator.store.currentUser).toEqual(mediatorMockData.currentUser);
+ expect(mediator.store.reviewers[0]).toEqual(mediatorMockData.currentUser);
+ });
+
+ describe('saves reviewers', () => {
+ const mockUpdateResponseData = {
+ reviewers: [1, 2],
+ assignees: [3, 4],
+ };
+ const field = 'merge_request[reviewers_ids]';
+ const reviewers = [
+ { id: 1, suggested: true },
+ { id: 2, suggested: false },
+ ];
+
+ let serviceSpy;
+
+ beforeEach(() => {
+ mediator.store.reviewers = reviewers;
+ serviceSpy = jest
+ .spyOn(mediator.service, 'update')
+ .mockReturnValue(Promise.resolve({ data: mockUpdateResponseData }));
+ });
+
+ it('sends correct data to service', () => {
+ const data = {
+ reviewer_ids: [1, 2],
+ suggested_reviewer_ids: [1],
+ };
+
+ mediator.saveReviewers(field);
+
+ expect(serviceSpy).toHaveBeenCalledWith(field, data);
+ });
+
+ it('saves reviewers', () => {
+ return mediator.saveReviewers(field).then(() => {
+ expect(mediator.store.assignees).toEqual(mockUpdateResponseData.assignees);
+ expect(mediator.store.reviewers).toEqual(mockUpdateResponseData.reviewers);
+ });
+ });
+ });
+
it('fetches the data', async () => {
const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
@@ -49,7 +96,6 @@ describe('Sidebar mediator', () => {
await mediator.fetch();
expect(spy).toHaveBeenCalledWith(mockData);
- spy.mockRestore();
});
it('processes fetched data', () => {
@@ -70,8 +116,6 @@ describe('Sidebar mediator', () => {
mediator.setMoveToProjectId(projectId);
expect(spy).toHaveBeenCalledWith(projectId);
-
- spy.mockRestore();
});
it('fetches autocomplete projects', () => {
@@ -87,9 +131,6 @@ describe('Sidebar mediator', () => {
return mediator.fetchAutocompleteProjects(searchTerm).then(() => {
expect(getterSpy).toHaveBeenCalledWith(searchTerm);
expect(setterSpy).toHaveBeenCalled();
-
- getterSpy.mockRestore();
- setterSpy.mockRestore();
});
});
@@ -106,9 +147,6 @@ describe('Sidebar mediator', () => {
return mediator.moveIssue().then(() => {
expect(moveIssueSpy).toHaveBeenCalledWith(moveToProjectId);
expect(urlSpy).toHaveBeenCalledWith(mockData.web_url);
-
- moveIssueSpy.mockRestore();
- urlSpy.mockRestore();
});
});
});
diff --git a/spec/frontend/sidebar/sidebar_store_spec.js b/spec/frontend/sidebar/stores/sidebar_store_spec.js
index 3930dabfcfa..3f4b80409c2 100644
--- a/spec/frontend/sidebar/sidebar_store_spec.js
+++ b/spec/frontend/sidebar/stores/sidebar_store_spec.js
@@ -1,6 +1,6 @@
import UsersMockHelper from 'helpers/user_mock_data_helper';
import SidebarStore from '~/sidebar/stores/sidebar_store';
-import Mock from './mock_data';
+import Mock from '../mock_data';
const ASSIGNEE = {
id: 2,
diff --git a/spec/frontend/terms/components/app_spec.js b/spec/frontend/terms/components/app_spec.js
index f1dbc004da8..ce1c126f868 100644
--- a/spec/frontend/terms/components/app_spec.js
+++ b/spec/frontend/terms/components/app_spec.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import { merge } from 'lodash';
import { GlIntersectionObserver } from '@gitlab/ui';
import { nextTick } from 'vue';
@@ -7,13 +6,14 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import { FLASH_TYPES, FLASH_CLOSED_EVENT } from '~/flash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import TermsApp from '~/terms/components/app.vue';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
jest.mock('~/lib/utils/common_utils');
+jest.mock('~/behaviors/markdown/render_gfm');
describe('TermsApp', () => {
let wrapper;
- let renderGFMSpy;
const defaultProvide = {
terms: 'foo bar',
@@ -35,7 +35,6 @@ describe('TermsApp', () => {
};
beforeEach(() => {
- renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
isLoggedIn.mockReturnValue(true);
});
@@ -65,7 +64,7 @@ describe('TermsApp', () => {
createComponent();
expect(wrapper.findByText(defaultProvide.terms).exists()).toBe(true);
- expect(renderGFMSpy).toHaveBeenCalled();
+ expect(renderGFM).toHaveBeenCalled();
});
describe('accept button', () => {
diff --git a/spec/frontend/terraform/components/init_command_modal_spec.js b/spec/frontend/terraform/components/init_command_modal_spec.js
index dbdff899bac..911bb8878da 100644
--- a/spec/frontend/terraform/components/init_command_modal_spec.js
+++ b/spec/frontend/terraform/components/init_command_modal_spec.js
@@ -7,12 +7,13 @@ const accessTokensPath = '/path/to/access-tokens-page';
const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1';
const username = 'username';
const modalId = 'fake-modal-id';
-const stateName = 'production';
+const stateName = 'aws/eu-central-1';
+const stateNameEncoded = encodeURIComponent(stateName);
const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
terraform init \\
- -backend-config="address=${terraformApiUrl}/${stateName}" \\
- -backend-config="lock_address=${terraformApiUrl}/${stateName}/lock" \\
- -backend-config="unlock_address=${terraformApiUrl}/${stateName}/lock" \\
+ -backend-config="address=${terraformApiUrl}/${stateNameEncoded}" \\
+ -backend-config="lock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\
+ -backend-config="unlock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\
-backend-config="username=${username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
@@ -61,9 +62,15 @@ describe('InitCommandModal', () => {
expect(findLink().attributes('href')).toBe(accessTokensPath);
});
- it('renders the init command with the username and state name prepopulated', () => {
- expect(findInitCommand().text()).toContain(username);
- expect(findInitCommand().text()).toContain(stateName);
+ describe('init command', () => {
+ it('includes correct address', () => {
+ expect(findInitCommand().text()).toContain(
+ `-backend-config="address=${terraformApiUrl}/${stateNameEncoded}"`,
+ );
+ });
+ it('includes correct username', () => {
+ expect(findInitCommand().text()).toContain(`-backend-config="username=${username}"`);
+ });
});
it('renders the copyToClipboard button', () => {
diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js
index 2eed1e30d0d..0c8ba266201 100644
--- a/spec/frontend/token_access/mock_data.js
+++ b/spec/frontend/token_access/mock_data.js
@@ -68,6 +68,19 @@ export const removeProjectSuccess = {
},
};
+export const updateScopeSuccess = {
+ data: {
+ ciCdSettingsUpdate: {
+ ciCdSettings: {
+ jobTokenScopeEnabled: false,
+ __typename: 'ProjectCiCdSetting',
+ },
+ errors: [],
+ __typename: 'CiCdSettingsUpdatePayload',
+ },
+ },
+};
+
export const mockProjects = [
{
id: '1',
diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/token_access_spec.js
index ea1d9db515a..6fe94e28548 100644
--- a/spec/frontend/token_access/token_access_spec.js
+++ b/spec/frontend/token_access/token_access_spec.js
@@ -8,6 +8,7 @@ import { createAlert } from '~/flash';
import TokenAccess from '~/token_access/components/token_access.vue';
import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql';
+import updateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql';
import getCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_ci_job_token_scope.query.graphql';
import getProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql';
import {
@@ -16,6 +17,7 @@ import {
projectsWithScope,
addProjectSuccess,
removeProjectSuccess,
+ updateScopeSuccess,
} from './mock_data';
const projectPath = 'root/my-repo';
@@ -31,11 +33,11 @@ describe('TokenAccess component', () => {
const enabledJobTokenScopeHandler = jest.fn().mockResolvedValue(enabledJobTokenScope);
const disabledJobTokenScopeHandler = jest.fn().mockResolvedValue(disabledJobTokenScope);
- const getProjectsWithScope = jest.fn().mockResolvedValue(projectsWithScope);
+ const getProjectsWithScopeHandler = jest.fn().mockResolvedValue(projectsWithScope);
const addProjectSuccessHandler = jest.fn().mockResolvedValue(addProjectSuccess);
- const addProjectFailureHandler = jest.fn().mockRejectedValue(error);
const removeProjectSuccessHandler = jest.fn().mockResolvedValue(removeProjectSuccess);
- const removeProjectFailureHandler = jest.fn().mockRejectedValue(error);
+ const updateScopeSuccessHandler = jest.fn().mockResolvedValue(updateScopeSuccess);
+ const failureHandler = jest.fn().mockRejectedValue(error);
const findToggle = () => wrapper.findComponent(GlToggle);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
@@ -69,7 +71,7 @@ describe('TokenAccess component', () => {
it('shows loading state while waiting on query to resolve', async () => {
createComponent([
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
]);
expect(findLoadingIcon().exists()).toBe(true);
@@ -80,11 +82,53 @@ describe('TokenAccess component', () => {
});
});
+ describe('fetching projects and scope', () => {
+ it('fetches projects and scope correctly', () => {
+ const expectedVariables = {
+ fullPath: 'root/my-repo',
+ };
+
+ createComponent([
+ [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ ]);
+
+ expect(enabledJobTokenScopeHandler).toHaveBeenCalledWith(expectedVariables);
+ expect(getProjectsWithScopeHandler).toHaveBeenCalledWith(expectedVariables);
+ });
+
+ it('handles fetch projects error correctly', async () => {
+ createComponent([
+ [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, failureHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'There was a problem fetching the projects',
+ });
+ });
+
+ it('handles fetch scope error correctly', async () => {
+ createComponent([
+ [getCIJobTokenScopeQuery, failureHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'There was a problem fetching the job token scope value',
+ });
+ });
+ });
+
describe('toggle', () => {
it('the toggle is on and the alert is hidden', async () => {
createComponent([
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
]);
await waitForPromises();
@@ -96,7 +140,7 @@ describe('TokenAccess component', () => {
it('the toggle is off and the alert is visible', async () => {
createComponent([
[getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
]);
await waitForPromises();
@@ -104,6 +148,47 @@ describe('TokenAccess component', () => {
expect(findToggle().props('value')).toBe(false);
expect(findTokenDisabledAlert().exists()).toBe(true);
});
+
+ describe('update ci job token scope', () => {
+ it('calls updateCIJobTokenScopeMutation mutation', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [updateCIJobTokenScopeMutation, updateScopeSuccessHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ findToggle().vm.$emit('change', false);
+
+ expect(updateScopeSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ fullPath: 'root/my-repo',
+ jobTokenScopeEnabled: false,
+ },
+ });
+ });
+
+ it('handles update scope error correctly', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
+ [updateCIJobTokenScopeMutation, failureHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ findToggle().vm.$emit('change', true);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message });
+ });
+ });
});
describe('add project', () => {
@@ -111,7 +196,7 @@ describe('TokenAccess component', () => {
createComponent(
[
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
[addProjectCIJobTokenScopeMutation, addProjectSuccessHandler],
],
mountExtended,
@@ -133,8 +218,8 @@ describe('TokenAccess component', () => {
createComponent(
[
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
- [addProjectCIJobTokenScopeMutation, addProjectFailureHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ [addProjectCIJobTokenScopeMutation, failureHandler],
],
mountExtended,
);
@@ -154,7 +239,7 @@ describe('TokenAccess component', () => {
createComponent(
[
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
[removeProjectCIJobTokenScopeMutation, removeProjectSuccessHandler],
],
mountExtended,
@@ -176,8 +261,8 @@ describe('TokenAccess component', () => {
createComponent(
[
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
- [removeProjectCIJobTokenScopeMutation, removeProjectFailureHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ [removeProjectCIJobTokenScopeMutation, failureHandler],
],
mountExtended,
);
diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
index bd40a968392..4077564486c 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
@@ -2,7 +2,7 @@
exports[`MRWidgetAutoMergeEnabled template should have correct elements 1`] = `
<div
- class="mr-widget-body media mr-widget-body-line-height-1 gl-line-height-normal"
+ class="mr-widget-body media gl-display-flex gl-align-items-center"
>
<div
class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3"
@@ -61,7 +61,7 @@ exports[`MRWidgetAutoMergeEnabled template should have correct elements 1`] = `
class="gl-display-flex gl-align-items-flex-start"
>
<div
- class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
+ class="dropdown b-dropdown gl-dropdown gl-display-block gl-md-display-none! btn-group"
lazy=""
no-caret=""
title="Options"
@@ -87,7 +87,7 @@ exports[`MRWidgetAutoMergeEnabled template should have correct elements 1`] = `
</svg>
<span
- class="gl-new-dropdown-button-text gl-sr-only"
+ class="gl-dropdown-button-text gl-sr-only"
>
</span>
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
index 06ee017dee7..270a37f87e7 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
@@ -1,9 +1,28 @@
-import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { shallowMount, mount } from '@vue/test-utils';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import api from '~/api';
+
+import showGlobalToast from '~/vue_shared/plugins/global_toast';
+
import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue';
import MrWidgetAuthorTime from '~/vue_merge_request_widget/components/mr_widget_author_time.vue';
import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
+import Actions from '~/vue_merge_request_widget/components/action_buttons.vue';
+
+import { MR_WIDGET_CLOSED_REOPEN_FAILURE } from '~/vue_merge_request_widget/i18n';
+
+jest.mock('~/api', () => ({
+ updateMergeRequest: jest.fn(),
+}));
+jest.mock('~/vue_shared/plugins/global_toast');
+
+useMockLocationHelper();
const MOCK_DATA = {
+ iid: 1,
metrics: {
mergedBy: {},
closedBy: {
@@ -19,22 +38,39 @@ const MOCK_DATA = {
},
targetBranchPath: '/twitter/flight/commits/so_long_jquery',
targetBranch: 'so_long_jquery',
+ targetProjectId: 'twitter/flight',
};
+function createComponent({ shallow = true, props = {} } = {}) {
+ const mounter = shallow ? shallowMount : mount;
+
+ return mounter(closedComponent, {
+ propsData: {
+ mr: MOCK_DATA,
+ ...props,
+ },
+ });
+}
+
+function findActions(wrapper) {
+ return wrapper.findComponent(StateContainer).findComponent(Actions);
+}
+
+function findReopenActionButton(wrapper) {
+ return findActions(wrapper).find('button[data-testid="extension-actions-reopen-button"]');
+}
+
describe('MRWidgetClosed', () => {
let wrapper;
beforeEach(() => {
- wrapper = shallowMount(closedComponent, {
- propsData: {
- mr: MOCK_DATA,
- },
- });
+ wrapper = createComponent();
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
+ if (wrapper) {
+ wrapper.destroy();
+ }
});
it('renders closed icon', () => {
@@ -51,4 +87,93 @@ describe('MRWidgetClosed', () => {
dateReadable: MOCK_DATA.metrics.readableClosedAt,
});
});
+
+ describe('actions', () => {
+ describe('reopen', () => {
+ beforeEach(() => {
+ window.gon = { current_user_id: 1 };
+ api.updateMergeRequest.mockResolvedValue(true);
+ wrapper = createComponent({ shallow: false });
+ });
+
+ it('shows the "reopen" button', () => {
+ expect(wrapper.findComponent(StateContainer).props().actions.length).toBe(1);
+ expect(findReopenActionButton(wrapper).text()).toBe('Reopen');
+ });
+
+ it('does not show widget actions when the user is not logged in', () => {
+ window.gon = {};
+
+ wrapper = createComponent();
+
+ expect(findActions(wrapper).exists()).toBe(false);
+ });
+
+ it('makes the reopen request with the correct MR information', async () => {
+ const reopenButton = findReopenActionButton(wrapper);
+
+ reopenButton.trigger('click');
+ await nextTick();
+
+ expect(api.updateMergeRequest).toHaveBeenCalledWith(
+ MOCK_DATA.targetProjectId,
+ MOCK_DATA.iid,
+ { state_event: 'reopen' },
+ );
+ });
+
+ it('shows "Reopening..." while the reopen network request is pending', async () => {
+ const reopenButton = findReopenActionButton(wrapper);
+
+ api.updateMergeRequest.mockReturnValue(new Promise(() => {}));
+
+ reopenButton.trigger('click');
+ await nextTick();
+
+ expect(reopenButton.text()).toBe('Reopening...');
+ });
+
+ it('shows "Refreshing..." when the reopen has succeeded', async () => {
+ const reopenButton = findReopenActionButton(wrapper);
+
+ reopenButton.trigger('click');
+ await waitForPromises();
+
+ expect(reopenButton.text()).toBe('Refreshing...');
+ });
+
+ it('reloads the page when a reopen has succeeded', async () => {
+ const reopenButton = findReopenActionButton(wrapper);
+
+ reopenButton.trigger('click');
+ await waitForPromises();
+
+ expect(window.location.reload).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows "Reopen" when a reopen request has failed', async () => {
+ const reopenButton = findReopenActionButton(wrapper);
+
+ api.updateMergeRequest.mockRejectedValue(false);
+
+ reopenButton.trigger('click');
+ await waitForPromises();
+
+ expect(window.location.reload).not.toHaveBeenCalled();
+ expect(reopenButton.text()).toBe('Reopen');
+ });
+
+ it('requests a toast popup when a reopen request has failed', async () => {
+ const reopenButton = findReopenActionButton(wrapper);
+
+ api.updateMergeRequest.mockRejectedValue(false);
+
+ reopenButton.trigger('click');
+ await waitForPromises();
+
+ expect(showGlobalToast).toHaveBeenCalledTimes(1);
+ expect(showGlobalToast).toHaveBeenCalledWith(MR_WIDGET_CLOSED_REOPEN_FAILURE);
+ });
+ });
+ });
});
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 407bd60b2b7..d34fc0c1e61 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
@@ -812,14 +812,30 @@ describe('ReadyToMerge', () => {
);
});
- it('shows the diverged commits text when the source branch is behind the target', () => {
- createComponent({
- mr: { divergedCommitsCount: 9001, userPermissions: { canMerge: false }, canMerge: false },
+ describe('shows the diverged commits text when the source branch is behind the target', () => {
+ it('when the MR can be merged', () => {
+ createComponent({
+ mr: { divergedCommitsCount: 9001 },
+ });
+
+ expect(wrapper.text()).toEqual(
+ expect.stringContaining('The source branch is 9001 commits behind the target branch'),
+ );
});
- expect(wrapper.text()).toEqual(
- expect.stringContaining('The source branch is 9001 commits behind the target branch'),
- );
+ it('when the MR cannot be merged', () => {
+ createComponent({
+ mr: {
+ divergedCommitsCount: 9001,
+ userPermissions: { canMerge: false },
+ canMerge: false,
+ },
+ });
+
+ expect(wrapper.text()).toEqual(
+ expect.stringContaining('The source branch is 9001 commits behind the target branch'),
+ );
+ });
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
new file mode 100644
index 00000000000..366ea113162
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
@@ -0,0 +1,47 @@
+import { GlButton, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Actions from '~/vue_merge_request_widget/components/widget/action_buttons.vue';
+
+let wrapper;
+
+function factory(propsData = {}) {
+ wrapper = shallowMount(Actions, {
+ propsData: { ...propsData, widget: 'test' },
+ });
+}
+
+describe('~/vue_merge_request_widget/components/widget/action_buttons.vue', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('tertiaryButtons', () => {
+ it('renders buttons', () => {
+ factory({
+ tertiaryButtons: [{ text: 'hello world', href: 'https://gitlab.com', target: '_blank' }],
+ });
+
+ expect(wrapper.findAllComponents(GlButton)).toHaveLength(1);
+ });
+
+ it('calls action click handler', async () => {
+ const onClick = jest.fn();
+
+ factory({
+ tertiaryButtons: [{ text: 'hello world', onClick }],
+ });
+
+ await wrapper.findComponent(GlButton).vm.$emit('click');
+
+ expect(onClick).toHaveBeenCalled();
+ });
+
+ it('renders tertiary actions in dropdown', () => {
+ factory({
+ tertiaryButtons: [{ text: 'hello world', href: 'https://gitlab.com', target: '_blank' }],
+ });
+
+ expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js
index e4bee6b8652..791fe541eb6 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js
@@ -1,7 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
import StatusIcon from '~/vue_merge_request_widget/components/widget/status_icon.vue';
-import ActionButtons from '~/vue_merge_request_widget/components/action_buttons.vue';
+import ActionButtons from '~/vue_merge_request_widget/components/widget/action_buttons.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
describe('~/vue_merge_request_widget/components/widget/widget_content_row.vue', () => {
@@ -76,7 +76,10 @@ describe('~/vue_merge_request_widget/components/widget/widget_content_row.vue',
},
});
- expect(findHelpPopover().props('options')).toEqual({ title: 'Help popover title' });
+ const popover = findHelpPopover();
+
+ expect(popover.props('options')).toEqual({ title: 'Help popover title' });
+ expect(popover.props('icon')).toBe('information-o');
expect(wrapper.findByText('Help popover content').exists()).toBe(true);
expect(wrapper.findByText('Learn more').attributes('href')).toBe('/path/to/docs');
expect(wrapper.findByText('Learn more').attributes('target')).toBe('_blank');
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 9635e050e4d..4c93c88de16 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
@@ -4,7 +4,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import waitForPromises from 'helpers/wait_for_promises';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
-import ActionButtons from '~/vue_merge_request_widget/components/action_buttons.vue';
+import ActionButtons from '~/vue_merge_request_widget/components/widget/action_buttons.vue';
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
@@ -24,6 +24,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
const findActionButtons = () => wrapper.findComponent(ActionButtons);
const findToggleButton = () => wrapper.findByTestId('toggle-button');
const findHelpPopover = () => wrapper.findComponent(HelpPopover);
+ const findDynamicScroller = () => wrapper.findByTestId('dynamic-content-scroller');
const createComponent = ({ propsData, slots } = {}) => {
wrapper = shallowMountExtended(Widget, {
@@ -212,7 +213,10 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
},
});
- expect(findHelpPopover().props('options')).toEqual({ title: 'My help popover title' });
+ const popover = findHelpPopover();
+
+ expect(popover.props('options')).toEqual({ title: 'My help popover title' });
+ expect(popover.props('icon')).toBe('information-o');
expect(wrapper.findByText('Help popover content').exists()).toBe(true);
expect(wrapper.findByText('Learn more').attributes('href')).toBe('/path/to/docs');
expect(wrapper.findByText('Learn more').attributes('target')).toBe('_blank');
@@ -370,7 +374,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
href: '#',
target: '_blank',
id: 'full-report-button',
- text: 'Full Report',
+ text: 'Full report',
},
],
},
@@ -388,7 +392,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('when full report is clicked it should call the respective telemetry event', async () => {
expect(wrapper.vm.telemetryHub.fullReportClicked).not.toHaveBeenCalled();
- wrapper.findByText('Full Report').vm.$emit('click');
+ wrapper.findByText('Full report').vm.$emit('click');
await nextTick();
expect(wrapper.vm.telemetryHub.fullReportClicked).toHaveBeenCalledTimes(1);
});
@@ -408,4 +412,30 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
expect(wrapper.vm.telemetryHub).toBe(null);
});
});
+
+ describe('dynamic content', () => {
+ const content = [
+ {
+ id: 'row-id',
+ header: ['This is a header', 'This is a subheader'],
+ text: 'Main text for the row',
+ subtext: 'Optional: Smaller sub-text to be displayed below the main text',
+ },
+ ];
+
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ content,
+ },
+ });
+ });
+
+ it('uses a dynamic scroller to show the items', async () => {
+ findToggleButton().vm.$emit('click');
+ await waitForPromises();
+ expect(findDynamicScroller().props('items')).toEqual(content);
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
index 05df66165dd..baef247b649 100644
--- a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
@@ -8,17 +8,17 @@ 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 httpStatusCodes from '~/lib/utils/http_status';
+import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
-import { failedReport } from 'jest/reports/mock_data/mock_data';
-import mixedResultsTestReports from 'jest/reports/mock_data/new_and_fixed_failures_report.json';
-import newErrorsTestReports from 'jest/reports/mock_data/new_errors_report.json';
-import newFailedTestReports from 'jest/reports/mock_data/new_failures_report.json';
-import newFailedTestWithNullFilesReport from 'jest/reports/mock_data/new_failures_with_null_files_report.json';
-import successTestReports from 'jest/reports/mock_data/no_failures_report.json';
-import resolvedFailures from 'jest/reports/mock_data/resolved_failures.json';
-import recentFailures from 'jest/reports/mock_data/recent_failures_report.json';
+import { failedReport } from 'jest/ci/reports/mock_data/mock_data';
+import mixedResultsTestReports from 'jest/ci/reports/mock_data/new_and_fixed_failures_report.json';
+import newErrorsTestReports from 'jest/ci/reports/mock_data/new_errors_report.json';
+import newFailedTestReports from 'jest/ci/reports/mock_data/new_failures_report.json';
+import newFailedTestWithNullFilesReport from 'jest/ci/reports/mock_data/new_failures_with_null_files_report.json';
+import successTestReports from 'jest/ci/reports/mock_data/no_failures_report.json';
+import resolvedFailures from 'jest/ci/reports/mock_data/resolved_failures.json';
+import recentFailures from 'jest/ci/reports/mock_data/recent_failures_report.json';
const reportWithParsingErrors = failedReport;
reportWithParsingErrors.suites[0].suite_errors = {
@@ -82,7 +82,7 @@ describe('Test report extension', () => {
});
it('with a 204 response, continues to display loading state', async () => {
- mockApi(httpStatusCodes.NO_CONTENT, '');
+ mockApi(HTTP_STATUS_NO_CONTENT, '');
createComponent();
await waitForPromises();
diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
index 9a72e4a086b..f0ebbb1a82e 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { GlBadge } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -6,10 +7,10 @@ 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 codeQualityExtension from '~/vue_merge_request_widget/extensions/code_quality';
-import httpStatusCodes from '~/lib/utils/http_status';
+import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
+import { i18n } from '~/vue_merge_request_widget/extensions/code_quality/constants';
import {
codeQualityResponseNewErrors,
- codeQualityResponseResolvedErrors,
codeQualityResponseResolvedAndNewErrors,
codeQualityResponseNoErrors,
} from './mock_data';
@@ -58,46 +59,55 @@ describe('Code Quality extension', () => {
createComponent();
- expect(wrapper.text()).toBe('Code Quality test metrics results are being parsed');
+ expect(wrapper.text()).toBe(i18n.loading);
});
- it('displays failed loading text', async () => {
- mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
-
+ it('with a 204 response, continues to display loading state', async () => {
+ mockApi(HTTP_STATUS_NO_CONTENT, '');
createComponent();
await waitForPromises();
- expect(wrapper.text()).toBe('Code Quality failed loading results');
+
+ expect(wrapper.text()).toBe(i18n.loading);
});
- it('displays quality degradation', async () => {
- mockApi(httpStatusCodes.OK, codeQualityResponseNewErrors);
+ it('displays failed loading text', async () => {
+ mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
createComponent();
await waitForPromises();
-
- expect(wrapper.text()).toBe('Code Quality degraded on 2 points.');
+ expect(wrapper.text()).toBe(i18n.error);
});
- it('displays quality improvement', async () => {
- mockApi(httpStatusCodes.OK, codeQualityResponseResolvedErrors);
+ it('displays correct single Report', async () => {
+ mockApi(httpStatusCodes.OK, codeQualityResponseNewErrors);
createComponent();
await waitForPromises();
- expect(wrapper.text()).toBe('Code Quality improved on 2 points.');
+ expect(wrapper.text()).toBe(
+ i18n.degradedCopy(i18n.singularReport(codeQualityResponseNewErrors.new_errors)),
+ );
});
it('displays quality improvement and degradation', async () => {
mockApi(httpStatusCodes.OK, codeQualityResponseResolvedAndNewErrors);
createComponent();
-
await waitForPromises();
- expect(wrapper.text()).toBe('Code Quality improved on 1 point and degraded on 1 point.');
+ // replacing strong tags because they will not be found in the rendered text
+ expect(wrapper.text()).toBe(
+ i18n
+ .improvementAndDegradationCopy(
+ i18n.pluralReport(codeQualityResponseResolvedAndNewErrors.resolved_errors),
+ i18n.pluralReport(codeQualityResponseResolvedAndNewErrors.new_errors),
+ )
+ .replace(/%{strong_start}/g, '')
+ .replace(/%{strong_end}/g, ''),
+ );
});
it('displays no detected errors', async () => {
@@ -107,7 +117,7 @@ describe('Code Quality extension', () => {
await waitForPromises();
- expect(wrapper.text()).toBe('No changes to Code Quality.');
+ expect(wrapper.text()).toBe(i18n.noChanges);
});
});
@@ -138,8 +148,17 @@ describe('Code Quality extension', () => {
"Minor - Parsing error: 'return' outside of function in index.js:12",
);
expect(text.resolvedError).toContain(
- "Minor - Parsing error: 'return' outside of function in index.js:12",
+ "Minor - Parsing error: 'return' outside of function Fixed in index.js:12",
);
});
+
+ it('adds fixed indicator (badge) when error is resolved', () => {
+ expect(findAllExtensionListItems().at(1).findComponent(GlBadge).exists()).toBe(true);
+ expect(findAllExtensionListItems().at(1).findComponent(GlBadge).text()).toEqual(i18n.fixed);
+ });
+
+ it('should not add fixed indicator (badge) when error is new', () => {
+ expect(findAllExtensionListItems().at(0).findComponent(GlBadge).exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
index f5ad0ce7377..2e8e70f25db 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
@@ -23,31 +23,6 @@ export const codeQualityResponseNewErrors = {
},
};
-export const codeQualityResponseResolvedErrors = {
- status: 'failed',
- new_errors: [],
- resolved_errors: [
- {
- description: "Parsing error: 'return' outside of function",
- severity: 'minor',
- file_path: 'index.js',
- line: 12,
- },
- {
- description: 'TODO found',
- severity: 'minor',
- file_path: '.gitlab-ci.yml',
- line: 73,
- },
- ],
- existing_errors: [],
- summary: {
- total: 2,
- resolved: 2,
- errored: 0,
- },
-};
-
export const codeQualityResponseResolvedAndNewErrors = {
status: 'failed',
new_errors: [
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 0f4637d18d9..683858b331d 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
@@ -22,6 +22,7 @@ import {
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
+import WidgetContainer from '~/vue_merge_request_widget/components/widget/app.vue';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
@@ -62,6 +63,7 @@ describe('MrWidgetOptions', () => {
let mock;
const COLLABORATION_MESSAGE = 'Members who can merge are allowed to add commits';
+ const findWidgetContainer = () => wrapper.findComponent(WidgetContainer);
const findExtensionToggleButton = () =>
wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]');
const findExtensionLink = (linkHref) =>
@@ -1228,5 +1230,22 @@ describe('MrWidgetOptions', () => {
expect(api.trackRedisCounterEvent).not.toHaveBeenCalled();
});
});
+
+ describe('widget container', () => {
+ afterEach(() => {
+ delete window.gon.features.refactorSecurityExtension;
+ });
+
+ it('should not be displayed when the refactor_security_extension feature flag is turned off', () => {
+ createComponent();
+ expect(findWidgetContainer().exists()).toBe(false);
+ });
+
+ it('should be displayed when the refactor_security_extension feature flag is turned on', () => {
+ window.gon.features.refactorSecurityExtension = true;
+ createComponent();
+ expect(findWidgetContainer().exists()).toBe(true);
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
deleted file mode 100644
index bdf5ea23812..00000000000
--- a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
+++ /dev/null
@@ -1,305 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
-<div
- class="awards js-awards-block"
->
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
- data-testid="award-button"
- title="Ada, Leonardo, and Marie reacted with :thumbsup:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="thumbsup"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 3
- </span>
- </span>
- </button>
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
- data-testid="award-button"
- title="You, Ada, and Marie reacted with :thumbsdown:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="thumbsdown"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 3
- </span>
- </span>
- </button>
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
- data-testid="award-button"
- title="Ada and Jane reacted with :smile:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="smile"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 2
- </span>
- </span>
- </button>
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
- data-testid="award-button"
- title="You, Ada, Jane, and Leonardo reacted with :ok_hand:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="ok_hand"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 4
- </span>
- </span>
- </button>
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
- data-testid="award-button"
- title="You reacted with :cactus:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="cactus"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 1
- </span>
- </span>
- </button>
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
- data-testid="award-button"
- title="Marie reacted with :a:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="a"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 1
- </span>
- </span>
- </button>
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
- data-testid="award-button"
- title="You reacted with :b:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="b"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 1
- </span>
- </span>
- </button>
-
- <div
- class="award-menu-holder gl-my-2"
- >
- <div
- class="emoji-picker"
- data-testid="emoji-picker"
- title="Add reaction"
- >
- <div
- boundary="scrollParent"
- class="dropdown b-dropdown gl-new-dropdown btn-group"
- id="__BVID__13"
- lazy=""
- menu-class="dropdown-extended-height"
- no-flip=""
- >
- <!---->
- <button
- aria-expanded="false"
- aria-haspopup="true"
- class="btn dropdown-toggle btn-default btn-md add-reaction-button btn-icon gl-relative! gl-button gl-dropdown-toggle btn-default-secondary"
- id="__BVID__13__BV_toggle_"
- type="button"
- >
- <span
- class="gl-sr-only"
- >
- Add reaction
- </span>
-
- <span
- class="reaction-control-icon reaction-control-icon-neutral"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="slight-smile-icon"
- role="img"
- >
- <use
- href="#slight-smile"
- />
- </svg>
- </span>
-
- <span
- class="reaction-control-icon reaction-control-icon-positive"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="smiley-icon"
- role="img"
- >
- <use
- href="#smiley"
- />
- </svg>
- </span>
-
- <span
- class="reaction-control-icon reaction-control-icon-super-positive"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="smile-icon"
- role="img"
- >
- <use
- href="#smile"
- />
- </svg>
- </span>
- </button>
- <ul
- aria-labelledby="__BVID__13__BV_toggle_"
- class="dropdown-menu dropdown-extended-height dropdown-menu-right"
- role="menu"
- tabindex="-1"
- >
- <!---->
- </ul>
- </div>
- </div>
- </div>
-</div>
-`;
diff --git a/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
index 87eaabf4e98..b7b43264330 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
@@ -7,6 +7,7 @@ exports[`MemoryGraph Render chart should draw container with chart 1`] = `
>
<gl-sparkline-chart-stub
data="Nov 12 2019 19:17:33,2.87,Nov 12 2019 19:18:33,2.78,Nov 12 2019 19:19:33,2.78,Nov 12 2019 19:20:33,3.01"
+ gradient=""
height="25"
tooltiplabel="MB"
/>
diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js
index 07c53c04723..f3fb840b270 100644
--- a/spec/frontend/vue_shared/components/actions_button_spec.js
+++ b/spec/frontend/vue_shared/components/actions_button_spec.js
@@ -1,6 +1,5 @@
-import { GlDropdown, GlDropdownDivider, GlButton } from '@gitlab/ui';
+import { GlDropdown, GlDropdownDivider, GlButton, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
const TEST_ACTION = {
@@ -32,7 +31,6 @@ describe('Actions button component', () => {
function createComponent(props) {
wrapper = shallowMount(ActionsButton, {
propsData: { ...props },
- directives: { GlTooltip: createMockDirective() },
});
}
@@ -40,15 +38,9 @@ describe('Actions button component', () => {
wrapper.destroy();
});
- const getTooltip = (child) => {
- const directiveBinding = getBinding(child.element, 'gl-tooltip');
-
- return directiveBinding.value;
- };
const findButton = () => wrapper.findComponent(GlButton);
- const findButtonTooltip = () => getTooltip(findButton());
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownTooltip = () => getTooltip(findDropdown());
const parseDropdownItems = () =>
findDropdown()
.findAll('gl-dropdown-item-stub,gl-dropdown-divider-stub')
@@ -88,8 +80,8 @@ describe('Actions button component', () => {
expect(findButton().text()).toBe(TEST_ACTION.text);
});
- it('should have tooltip', () => {
- expect(findButtonTooltip()).toBe(TEST_ACTION.tooltip);
+ it('should not have tooltip', () => {
+ expect(findTooltip().exists()).toBe(false);
});
it('should have attrs', () => {
@@ -105,7 +97,18 @@ describe('Actions button component', () => {
it('should have tooltip', () => {
createComponent({ actions: [{ ...TEST_ACTION, tooltip: TEST_TOOLTIP }] });
- expect(findButtonTooltip()).toBe(TEST_TOOLTIP);
+ expect(findTooltip().text()).toBe(TEST_TOOLTIP);
+ });
+ });
+
+ describe('when showActionTooltip is false', () => {
+ it('should not have tooltip', () => {
+ createComponent({
+ actions: [{ ...TEST_ACTION, tooltip: TEST_TOOLTIP }],
+ showActionTooltip: false,
+ });
+
+ expect(findTooltip().exists()).toBe(false);
});
});
@@ -174,8 +177,8 @@ describe('Actions button component', () => {
expect(wrapper.emitted('select')).toEqual([[TEST_ACTION_2.key]]);
});
- it('should have tooltip value', () => {
- expect(findDropdownTooltip()).toBe(TEST_ACTION.tooltip);
+ it('should not have tooltip value', () => {
+ expect(findTooltip().exists()).toBe(false);
});
});
@@ -199,7 +202,7 @@ describe('Actions button component', () => {
});
it('should have tooltip value', () => {
- expect(findDropdownTooltip()).toBe(TEST_ACTION_2.tooltip);
+ expect(findTooltip().text()).toBe(TEST_ACTION_2.tooltip);
});
});
});
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index 1c8cf726aca..c7f9d8fd8d5 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -38,7 +38,18 @@ const TEST_AWARDS = [
createAward(EMOJI_CACTUS, USERS.root),
createAward(EMOJI_A, USERS.marie),
createAward(EMOJI_B, USERS.root),
+ createAward(EMOJI_100, USERS.ada),
];
+const TEST_AWARDS_LENGTH = [
+ EMOJI_SMILE,
+ EMOJI_OK,
+ EMOJI_THUMBSUP,
+ EMOJI_THUMBSDOWN,
+ EMOJI_A,
+ EMOJI_B,
+ EMOJI_CACTUS,
+ EMOJI_100,
+].length;
const TEST_ADD_BUTTON_CLASS = 'js-test-add-button-class';
const REACTION_CONTROL_CLASSES = [
@@ -88,10 +99,6 @@ describe('vue_shared/components/awards_list', () => {
});
});
- it('matches snapshot', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
it('shows awards in correct order', () => {
expect(findAwardsData()).toEqual([
{
@@ -108,6 +115,12 @@ describe('vue_shared/components/awards_list', () => {
},
{
classes: REACTION_CONTROL_CLASSES,
+ count: 1,
+ html: matchingEmojiTag(EMOJI_100),
+ title: `Ada reacted with :${EMOJI_100}:`,
+ },
+ {
+ classes: REACTION_CONTROL_CLASSES,
count: 2,
html: matchingEmojiTag(EMOJI_SMILE),
title: `Ada and Jane reacted with :${EMOJI_SMILE}:`,
@@ -142,33 +155,23 @@ describe('vue_shared/components/awards_list', () => {
it('with award clicked, it emits award', () => {
expect(wrapper.emitted().award).toBeUndefined();
- findAwardButtons().at(2).vm.$emit('click');
+ findAwardButtons().at(3).vm.$emit('click');
expect(wrapper.emitted().award).toEqual([[EMOJI_SMILE]]);
});
- it('shows add award button', () => {
- const btn = findAddAwardButton();
+ it('with numeric award clicked, it emits award as is', () => {
+ expect(wrapper.emitted().award).toBeUndefined();
- expect(btn.exists()).toBe(true);
- });
- });
+ findAwardButtons().at(2).vm.$emit('click');
- describe('with numeric award', () => {
- beforeEach(() => {
- createComponent({
- awards: [createAward(EMOJI_100, USERS.ada)],
- canAwardEmoji: true,
- currentUserId: USERS.root.id,
- });
+ expect(wrapper.emitted().award).toEqual([[EMOJI_100]]);
});
- it('when clicked, it emits award as number', () => {
- expect(wrapper.emitted().award).toBeUndefined();
-
- findAwardButtons().at(0).vm.$emit('click');
+ it('shows add award button', () => {
+ const btn = findAddAwardButton();
- expect(wrapper.emitted().award).toEqual([[Number(EMOJI_100)]]);
+ expect(btn.exists()).toBe(true);
});
});
@@ -210,7 +213,7 @@ describe('vue_shared/components/awards_list', () => {
it('disables award buttons', () => {
const buttons = findAwardButtons();
- expect(buttons.length).toBe(7);
+ expect(buttons.length).toBe(TEST_AWARDS_LENGTH);
expect(buttons.wrappers.every((x) => x.classes('disabled'))).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
index f28805471f8..a37071aec9b 100644
--- a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -1,7 +1,8 @@
import { mount } from '@vue/test-utils';
import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
-import '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
describe('ContentViewer', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
index 01ef52c6af9..0d329b6a065 100644
--- a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
@@ -1,11 +1,12 @@
import { GlSkeletonLoader } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import $ from 'jquery';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import MarkdownViewer from '~/vue_shared/components/content_viewer/viewers/markdown_viewer.vue';
+jest.mock('~/behaviors/markdown/render_gfm');
+
describe('MarkdownViewer', () => {
let wrapper;
let mock;
@@ -26,7 +27,6 @@ describe('MarkdownViewer', () => {
mock = new MockAdapter(axios);
jest.spyOn(axios, 'post');
- jest.spyOn($.fn, 'renderGFM');
});
afterEach(() => {
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 1b9ca8e6092..b0e393bbf5e 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
@@ -13,7 +13,10 @@ import RecentSearchesService from '~/filtered_search/services/recent_searches_se
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import {
FILTERED_SEARCH_TERM,
- SortDirection,
+ SORT_DIRECTION,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { uniqueTokens } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
@@ -87,7 +90,7 @@ describe('FilteredSearchBarRoot', () => {
it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props and displays the sort dropdown', () => {
expect(wrapper.vm.filterValue).toEqual([]);
expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0]);
- expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending);
+ expect(wrapper.vm.selectedSortDirection).toBe(SORT_DIRECTION.descending);
expect(wrapper.findComponent(GlButtonGroup).exists()).toBe(true);
expect(wrapper.findComponent(GlButton).exists()).toBe(true);
expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
@@ -110,9 +113,9 @@ describe('FilteredSearchBarRoot', () => {
describe('tokenSymbols', () => {
it('returns a map containing type and symbols from `tokens` prop', () => {
expect(wrapper.vm.tokenSymbols).toEqual({
- author_username: '@',
- label_name: '~',
- milestone_title: '%',
+ [TOKEN_TYPE_AUTHOR]: '@',
+ [TOKEN_TYPE_LABEL]: '~',
+ [TOKEN_TYPE_MILESTONE]: '%',
});
});
});
@@ -120,9 +123,9 @@ describe('FilteredSearchBarRoot', () => {
describe('tokenTitles', () => {
it('returns a map containing type and title from `tokens` prop', () => {
expect(wrapper.vm.tokenTitles).toEqual({
- author_username: 'Author',
- label_name: 'Label',
- milestone_title: 'Milestone',
+ [TOKEN_TYPE_AUTHOR]: 'Author',
+ [TOKEN_TYPE_LABEL]: 'Label',
+ [TOKEN_TYPE_MILESTONE]: 'Milestone',
});
});
});
@@ -132,7 +135,7 @@ describe('FilteredSearchBarRoot', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
- selectedSortDirection: SortDirection.ascending,
+ selectedSortDirection: SORT_DIRECTION.ascending,
});
expect(wrapper.vm.sortDirectionIcon).toBe('sort-lowest');
@@ -142,7 +145,7 @@ describe('FilteredSearchBarRoot', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
- selectedSortDirection: SortDirection.descending,
+ selectedSortDirection: SORT_DIRECTION.descending,
});
expect(wrapper.vm.sortDirectionIcon).toBe('sort-highest');
@@ -154,7 +157,7 @@ describe('FilteredSearchBarRoot', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
- selectedSortDirection: SortDirection.ascending,
+ selectedSortDirection: SORT_DIRECTION.ascending,
});
expect(wrapper.vm.sortDirectionTooltip).toBe('Sort direction: Ascending');
@@ -164,7 +167,7 @@ describe('FilteredSearchBarRoot', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
- selectedSortDirection: SortDirection.descending,
+ selectedSortDirection: SORT_DIRECTION.descending,
});
expect(wrapper.vm.sortDirectionTooltip).toBe('Sort direction: Descending');
@@ -272,11 +275,11 @@ describe('FilteredSearchBarRoot', () => {
});
it('sets `selectedSortDirection` to be opposite of its current value', () => {
- expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending);
+ expect(wrapper.vm.selectedSortDirection).toBe(SORT_DIRECTION.descending);
wrapper.vm.handleSortDirectionClick();
- expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.ascending);
+ expect(wrapper.vm.selectedSortDirection).toBe(SORT_DIRECTION.ascending);
});
it('emits component event `onSort` with opposite of currently selected sort by value', () => {
@@ -384,7 +387,7 @@ describe('FilteredSearchBarRoot', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortOption: mockSortOptions[0],
- selectedSortDirection: SortDirection.descending,
+ selectedSortDirection: SORT_DIRECTION.descending,
recentSearches: mockHistoryItems,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
index a6713b7e7e4..b2f4c780f51 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -1,8 +1,28 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
-import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
+import { mockLabels } from 'jest/sidebar/components/labels/labels_select_vue/mock_data';
import Api from '~/api';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import {
+ FILTERED_SEARCH_TERM,
+ OPERATORS_IS,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_CONTACT,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_MY_REACTION,
+ TOKEN_TITLE_ORGANIZATION,
+ TOKEN_TITLE_RELEASE,
+ TOKEN_TITLE_SOURCE_BRANCH,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_CONTACT,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_ORGANIZATION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_SOURCE_BRANCH,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
@@ -11,7 +31,7 @@ import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/rel
import CrmContactToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue';
import CrmOrganizationToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue';
-export const mockAuthor1 = {
+export const mockUser1 = {
id: 1,
name: 'Administrator',
username: 'root',
@@ -20,7 +40,7 @@ export const mockAuthor1 = {
web_url: 'http://0.0.0.0:3000/root',
};
-export const mockAuthor2 = {
+export const mockUser2 = {
id: 2,
name: 'Claudio Beer',
username: 'ericka_terry',
@@ -29,7 +49,7 @@ export const mockAuthor2 = {
web_url: 'http://0.0.0.0:3000/ericka_terry',
};
-export const mockAuthor3 = {
+export const mockUser3 = {
id: 6,
name: 'Shizue Hartmann',
username: 'junita.weimann',
@@ -38,7 +58,7 @@ export const mockAuthor3 = {
web_url: 'http://0.0.0.0:3000/junita.weimann',
};
-export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3];
+export const mockUsers = [mockUser1, mockUser2, mockUser3];
export const mockBranches = [{ name: 'Main' }, { name: 'v1.x' }, { name: 'my-Branch' }];
@@ -197,86 +217,86 @@ export const mockEmoji2 = {
export const mockEmojis = [mockEmoji1, mockEmoji2];
export const mockBranchToken = {
- type: 'source_branch',
+ type: TOKEN_TYPE_SOURCE_BRANCH,
icon: 'branch',
- title: 'Source Branch',
+ title: TOKEN_TITLE_SOURCE_BRANCH,
unique: true,
token: BranchToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
fetchBranches: Api.branches.bind(Api),
};
export const mockAuthorToken = {
- type: 'author_username',
+ type: TOKEN_TYPE_AUTHOR,
icon: 'user',
- title: 'Author',
+ title: TOKEN_TITLE_AUTHOR,
unique: false,
symbol: '@',
- token: AuthorToken,
- operators: OPERATOR_IS_ONLY,
+ token: UserToken,
+ operators: OPERATORS_IS,
fetchPath: 'gitlab-org/gitlab-test',
- fetchAuthors: Api.projectUsers.bind(Api),
+ fetchUsers: Api.projectUsers.bind(Api),
};
export const mockLabelToken = {
- type: 'label_name',
+ type: TOKEN_TYPE_LABEL,
icon: 'labels',
- title: 'Label',
+ title: TOKEN_TITLE_LABEL,
unique: false,
symbol: '~',
token: LabelToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
fetchLabels: () => Promise.resolve(mockLabels),
};
export const mockMilestoneToken = {
- type: 'milestone_title',
+ type: TOKEN_TYPE_MILESTONE,
icon: 'clock',
- title: 'Milestone',
+ title: TOKEN_TITLE_MILESTONE,
unique: true,
symbol: '%',
token: MilestoneToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
fetchMilestones: () => Promise.resolve({ data: mockMilestones }),
};
export const mockReleaseToken = {
- type: 'release',
+ type: TOKEN_TYPE_RELEASE,
icon: 'rocket',
- title: 'Release',
+ title: TOKEN_TITLE_RELEASE,
token: ReleaseToken,
fetchReleases: () => Promise.resolve(),
};
export const mockReactionEmojiToken = {
- type: 'my_reaction_emoji',
+ type: TOKEN_TYPE_MY_REACTION,
icon: 'thumb-up',
- title: 'My-Reaction',
+ title: TOKEN_TITLE_MY_REACTION,
unique: true,
token: EmojiToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
fetchEmojis: () => Promise.resolve(mockEmojis),
};
export const mockCrmContactToken = {
- type: 'crm_contact',
- title: 'Contact',
+ type: TOKEN_TYPE_CONTACT,
+ title: TOKEN_TITLE_CONTACT,
icon: 'user',
token: CrmContactToken,
isProject: false,
fullPath: 'group',
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
unique: true,
};
export const mockCrmOrganizationToken = {
- type: 'crm_contact',
- title: 'Organization',
+ type: TOKEN_TYPE_ORGANIZATION,
+ title: TOKEN_TITLE_ORGANIZATION,
icon: 'user',
token: CrmOrganizationToken,
isProject: false,
fullPath: 'group',
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
unique: true,
};
@@ -286,7 +306,7 @@ export const mockMembershipToken = {
title: 'Membership',
token: GlFilteredSearchToken,
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
options: [
{ value: 'exclude', title: 'Direct' },
{ value: 'only', title: 'Inherited' },
@@ -301,7 +321,7 @@ export const mockMembershipTokenOptionsWithoutTitles = {
export const mockAvailableTokens = [mockAuthorToken, mockLabelToken, mockMilestoneToken];
export const tokenValueAuthor = {
- type: 'author_username',
+ type: TOKEN_TYPE_AUTHOR,
value: {
data: 'root',
operator: '=',
@@ -309,7 +329,7 @@ export const tokenValueAuthor = {
};
export const tokenValueLabel = {
- type: 'label_name',
+ type: TOKEN_TYPE_LABEL,
value: {
operator: '=',
data: 'bug',
@@ -317,7 +337,7 @@ export const tokenValueLabel = {
};
export const tokenValueMilestone = {
- type: 'milestone_title',
+ type: TOKEN_TYPE_MILESTONE,
value: {
operator: '=',
data: 'v1.0',
@@ -333,7 +353,7 @@ export const tokenValueMembership = {
};
export const tokenValueConfidential = {
- type: 'confidential',
+ type: TOKEN_TYPE_CONFIDENTIAL,
value: {
operator: '=',
data: true,
@@ -341,23 +361,10 @@ export const tokenValueConfidential = {
};
export const tokenValuePlain = {
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: 'foo' },
};
-export const tokenValueEmpty = {
- type: 'filtered-search-term',
- value: { data: '' },
-};
-
-export const tokenValueEpic = {
- type: 'epic_iid',
- value: {
- operator: '=',
- data: '"foo"::&42',
- },
-};
-
export const mockHistoryItems = [
[tokenValueAuthor, tokenValueLabel, tokenValueMilestone, 'duo'],
[tokenValueAuthor, 'si'],
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 a0126c2bd63..164235e4bb9 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
@@ -12,12 +12,12 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
mockRegularLabel,
mockLabels,
-} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
+} from 'jest/sidebar/components/labels/labels_select_vue/mock_data';
import {
- DEFAULT_NONE_ANY,
+ OPTIONS_NONE_ANY,
OPERATOR_IS,
- OPERATOR_IS_NOT,
+ OPERATOR_NOT,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
getRecentlyUsedSuggestions,
@@ -76,7 +76,7 @@ const mockProps = {
active: false,
suggestions: [],
suggestionsLoading: false,
- defaultSuggestions: DEFAULT_NONE_ANY,
+ defaultSuggestions: OPTIONS_NONE_ANY,
getActiveTokenValue: (labels, data) => labels.find((label) => label.title === data),
cursorPosition: 'start',
};
@@ -301,13 +301,13 @@ describe('BaseToken', () => {
describe('with default suggestions', () => {
describe.each`
- operator | shouldRenderFilteredSearchSuggestion
- ${OPERATOR_IS} | ${true}
- ${OPERATOR_IS_NOT} | ${false}
+ operator | shouldRenderFilteredSearchSuggestion
+ ${OPERATOR_IS} | ${true}
+ ${OPERATOR_NOT} | ${false}
`('when operator is $operator', ({ shouldRenderFilteredSearchSuggestion, operator }) => {
beforeEach(() => {
const props = {
- defaultSuggestions: DEFAULT_NONE_ANY,
+ defaultSuggestions: OPTIONS_NONE_ANY,
value: { data: '', operator },
};
@@ -322,7 +322,7 @@ describe('BaseToken', () => {
if (shouldRenderFilteredSearchSuggestion) {
expect(filteredSearchSuggestions.map((c) => c.props())).toMatchObject(
- DEFAULT_NONE_ANY.map((opt) => ({ value: opt.value })),
+ OPTIONS_NONE_ANY.map((opt) => ({ value: opt.value })),
);
} else {
expect(filteredSearchSuggestions).toHaveLength(0);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index 05b42011fe1..311d5a13280 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -11,7 +11,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
import { mockBranches, mockBranchToken } from '../mock_data';
@@ -112,7 +112,7 @@ describe('BranchToken', () => {
});
describe('template', () => {
- const defaultBranches = DEFAULT_NONE_ANY;
+ const defaultBranches = OPTIONS_NONE_ANY;
async function showSuggestions() {
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
index 5b744521979..7be7035a0f2 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
@@ -10,7 +10,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import CrmContactToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue';
import searchCrmContactsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_crm_contacts.query.graphql';
@@ -187,7 +187,7 @@ describe('CrmContactToken', () => {
});
describe('template', () => {
- const defaultContacts = DEFAULT_NONE_ANY;
+ const defaultContacts = OPTIONS_NONE_ANY;
it('renders base-token component', () => {
mountComponent({
@@ -250,7 +250,7 @@ describe('CrmContactToken', () => {
expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
- it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
+ it('renders `OPTIONS_NONE_ANY` as default suggestions', () => {
mountComponent({
active: true,
config: { ...mockCrmContactToken },
@@ -262,8 +262,8 @@ describe('CrmContactToken', () => {
const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
- expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
- DEFAULT_NONE_ANY.forEach((contact, index) => {
+ expect(suggestions).toHaveLength(OPTIONS_NONE_ANY.length);
+ OPTIONS_NONE_ANY.forEach((contact, index) => {
expect(suggestions.at(index).text()).toBe(contact.text);
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
index 3a3e96032e8..ecd3e8a04f1 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
@@ -10,7 +10,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import CrmOrganizationToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue';
import searchCrmOrganizationsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_crm_organizations.query.graphql';
@@ -186,7 +186,7 @@ describe('CrmOrganizationToken', () => {
});
describe('template', () => {
- const defaultOrganizations = DEFAULT_NONE_ANY;
+ const defaultOrganizations = OPTIONS_NONE_ANY;
it('renders base-token component', () => {
mountComponent({
@@ -249,7 +249,7 @@ describe('CrmOrganizationToken', () => {
expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
- it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
+ it('renders `OPTIONS_NONE_ANY` as default suggestions', () => {
mountComponent({
active: true,
config: { ...mockCrmOrganizationToken },
@@ -261,8 +261,8 @@ describe('CrmOrganizationToken', () => {
const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
- expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
- DEFAULT_NONE_ANY.forEach((organization, index) => {
+ expect(suggestions).toHaveLength(OPTIONS_NONE_ANY.length);
+ OPTIONS_NONE_ANY.forEach((organization, index) => {
expect(suggestions.at(index).text()).toBe(organization.text);
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index e8436d2db17..773df01ada7 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -12,9 +12,9 @@ import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
- DEFAULT_LABEL_NONE,
- DEFAULT_LABEL_ANY,
- DEFAULT_NONE_ANY,
+ OPTION_NONE,
+ OPTION_ANY,
+ OPTIONS_NONE_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
@@ -118,7 +118,7 @@ describe('EmojiToken', () => {
});
describe('template', () => {
- const defaultEmojis = DEFAULT_NONE_ANY;
+ const defaultEmojis = OPTIONS_NONE_ANY;
beforeEach(async () => {
wrapper = createComponent({
@@ -181,7 +181,7 @@ describe('EmojiToken', () => {
expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
- it('renders `DEFAULT_LABEL_NONE` and `DEFAULT_LABEL_ANY` as default suggestions', async () => {
+ it('renders `OPTION_NONE` and `OPTION_ANY` as default suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockReactionEmojiToken },
@@ -195,8 +195,8 @@ describe('EmojiToken', () => {
const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(2);
- expect(suggestions.at(0).text()).toBe(DEFAULT_LABEL_NONE.text);
- expect(suggestions.at(1).text()).toBe(DEFAULT_LABEL_ANY.text);
+ expect(suggestions.at(0).text()).toBe(OPTION_NONE.text);
+ expect(suggestions.at(1).text()).toBe(OPTION_ANY.text);
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index 8ca12afacec..9d96123c17f 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -10,11 +10,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import {
mockRegularLabel,
mockLabels,
-} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
+} from 'jest/sidebar/components/labels/labels_select_vue/mock_data';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
@@ -141,7 +141,7 @@ describe('LabelToken', () => {
});
describe('template', () => {
- const defaultLabels = DEFAULT_NONE_ANY;
+ const defaultLabels = OPTIONS_NONE_ANY;
beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
@@ -209,7 +209,7 @@ describe('LabelToken', () => {
expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
- it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
+ it('renders `OPTIONS_NONE_ANY` as default suggestions', () => {
wrapper = createComponent({
active: true,
config: { ...mockLabelToken },
@@ -221,8 +221,8 @@ describe('LabelToken', () => {
const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
- expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
- DEFAULT_NONE_ANY.forEach((label, index) => {
+ expect(suggestions).toHaveLength(OPTIONS_NONE_ANY.length);
+ OPTIONS_NONE_ANY.forEach((label, index) => {
expect(suggestions.at(index).text()).toBe(label.text);
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
index 5371b9af475..32cb74d5f80 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
@@ -11,11 +11,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { mockAuthorToken, mockAuthors } from '../mock_data';
+import { mockAuthorToken, mockUsers } from '../mock_data';
jest.mock('~/flash');
const defaultStubs = {
@@ -28,7 +28,7 @@ const defaultStubs = {
},
};
-const mockPreloadedAuthors = [
+const mockPreloadedUsers = [
{
id: 13,
name: 'Administrator',
@@ -46,7 +46,7 @@ function createComponent(options = {}) {
data = {},
listeners = {},
} = options;
- return mount(AuthorToken, {
+ return mount(UserToken, {
propsData: {
config,
value,
@@ -66,7 +66,7 @@ function createComponent(options = {}) {
});
}
-describe('AuthorToken', () => {
+describe('UserToken', () => {
const originalGon = window.gon;
const currentUserLength = 1;
let mock;
@@ -85,40 +85,40 @@ describe('AuthorToken', () => {
});
describe('methods', () => {
- describe('fetchAuthors', () => {
+ describe('fetchUsers', () => {
beforeEach(() => {
wrapper = createComponent();
});
- it('calls `config.fetchAuthors` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchAuthors');
+ it('calls `config.fetchUsers` with provided searchTerm param', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchUsers');
- getBaseToken().vm.$emit('fetch-suggestions', mockAuthors[0].username);
+ getBaseToken().vm.$emit('fetch-suggestions', mockUsers[0].username);
- expect(wrapper.vm.config.fetchAuthors).toHaveBeenCalledWith(
+ expect(wrapper.vm.config.fetchUsers).toHaveBeenCalledWith(
mockAuthorToken.fetchPath,
- mockAuthors[0].username,
+ mockUsers[0].username,
);
});
- it('sets response to `authors` when request is succesful', () => {
- jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockResolvedValue(mockAuthors);
+ it('sets response to `users` when request is successful', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchUsers').mockResolvedValue(mockUsers);
getBaseToken().vm.$emit('fetch-suggestions', 'root');
return waitForPromises().then(() => {
- expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
+ expect(getBaseToken().props('suggestions')).toEqual(mockUsers);
});
});
// TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
describe('when there are null users presents', () => {
- const mockAuthorsWithNullUser = mockAuthors.concat([null]);
+ const mockUsersWithNullUser = mockUsers.concat([null]);
beforeEach(() => {
jest
- .spyOn(wrapper.vm.config, 'fetchAuthors')
- .mockResolvedValue({ data: mockAuthorsWithNullUser });
+ .spyOn(wrapper.vm.config, 'fetchUsers')
+ .mockResolvedValue({ data: mockUsersWithNullUser });
getBaseToken().vm.$emit('fetch-suggestions', 'root');
});
@@ -126,7 +126,7 @@ describe('AuthorToken', () => {
describe('when res.data is present', () => {
it('filters the successful response when null values are present', () => {
return waitForPromises().then(() => {
- expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
+ expect(getBaseToken().props('suggestions')).toEqual(mockUsers);
});
});
});
@@ -134,14 +134,14 @@ describe('AuthorToken', () => {
describe('when response is an array', () => {
it('filters the successful response when null values are present', () => {
return waitForPromises().then(() => {
- expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
+ expect(getBaseToken().props('suggestions')).toEqual(mockUsers);
});
});
});
});
it('calls `createAlert` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
+ jest.spyOn(wrapper.vm.config, 'fetchUsers').mockRejectedValue({});
getBaseToken().vm.$emit('fetch-suggestions', 'root');
@@ -153,7 +153,7 @@ describe('AuthorToken', () => {
});
it('sets `loading` to false when request completes', async () => {
- jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
+ jest.spyOn(wrapper.vm.config, 'fetchUsers').mockRejectedValue({});
getBaseToken().vm.$emit('fetch-suggestions', 'root');
@@ -174,23 +174,23 @@ describe('AuthorToken', () => {
it('renders base-token component', () => {
wrapper = createComponent({
- value: { data: mockAuthors[0].username },
- data: { authors: mockAuthors },
+ value: { data: mockUsers[0].username },
+ data: { users: mockUsers },
});
const baseTokenEl = getBaseToken();
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
- suggestions: mockAuthors,
- getActiveTokenValue: wrapper.vm.getActiveAuthor,
+ suggestions: mockUsers,
+ getActiveTokenValue: wrapper.vm.getActiveUser,
});
});
it('renders token item when value is selected', async () => {
wrapper = createComponent({
- value: { data: mockAuthors[0].username },
- data: { authors: mockAuthors },
+ value: { data: mockUsers[0].username },
+ data: { users: mockUsers },
stubs: { Portal: true },
});
@@ -201,20 +201,20 @@ describe('AuthorToken', () => {
const tokenValue = tokenSegments.at(2);
- expect(tokenValue.findComponent(GlAvatar).props('src')).toBe(mockAuthors[0].avatar_url);
- expect(tokenValue.text()).toBe(mockAuthors[0].name); // "Administrator"
+ expect(tokenValue.findComponent(GlAvatar).props('src')).toBe(mockUsers[0].avatar_url);
+ expect(tokenValue.text()).toBe(mockUsers[0].name); // "Administrator"
});
- it('renders token value with correct avatarUrl from author object', async () => {
+ it('renders token value with correct avatarUrl from user object', async () => {
const getAvatarEl = () =>
wrapper.findAllComponents(GlFilteredSearchTokenSegment).at(2).findComponent(GlAvatar);
wrapper = createComponent({
- value: { data: mockAuthors[0].username },
+ value: { data: mockUsers[0].username },
data: {
- authors: [
+ users: [
{
- ...mockAuthors[0],
+ ...mockUsers[0],
},
],
},
@@ -223,15 +223,15 @@ describe('AuthorToken', () => {
await nextTick();
- expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url);
+ expect(getAvatarEl().props('src')).toBe(mockUsers[0].avatar_url);
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
- authors: [
+ users: [
{
- ...mockAuthors[0],
- avatarUrl: mockAuthors[0].avatar_url,
+ ...mockUsers[0],
+ avatarUrl: mockUsers[0].avatar_url,
avatar_url: undefined,
},
],
@@ -239,14 +239,14 @@ describe('AuthorToken', () => {
await nextTick();
- expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url);
+ expect(getAvatarEl().props('src')).toBe(mockUsers[0].avatar_url);
});
- it('renders provided defaultAuthors as suggestions', async () => {
- const defaultAuthors = DEFAULT_NONE_ANY;
+ it('renders provided defaultUsers as suggestions', async () => {
+ const defaultUsers = OPTIONS_NONE_ANY;
wrapper = createComponent({
active: true,
- config: { ...mockAuthorToken, defaultAuthors, preloadedAuthors: mockPreloadedAuthors },
+ config: { ...mockAuthorToken, defaultUsers, preloadedUsers: mockPreloadedUsers },
stubs: { Portal: true },
});
@@ -254,16 +254,16 @@ describe('AuthorToken', () => {
const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
- expect(suggestions).toHaveLength(defaultAuthors.length + currentUserLength);
- defaultAuthors.forEach((label, index) => {
+ expect(suggestions).toHaveLength(defaultUsers.length + currentUserLength);
+ defaultUsers.forEach((label, index) => {
expect(suggestions.at(index).text()).toBe(label.text);
});
});
- it('does not render divider when no defaultAuthors', async () => {
+ it('does not render divider when no defaultUsers', async () => {
wrapper = createComponent({
active: true,
- config: { ...mockAuthorToken, defaultAuthors: [] },
+ config: { ...mockAuthorToken, defaultUsers: [] },
stubs: { Portal: true },
});
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
@@ -274,10 +274,10 @@ describe('AuthorToken', () => {
expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
- it('renders `DEFAULT_NONE_ANY` as default suggestions', async () => {
+ it('renders `OPTIONS_NONE_ANY` as default suggestions', async () => {
wrapper = createComponent({
active: true,
- config: { ...mockAuthorToken, preloadedAuthors: mockPreloadedAuthors },
+ config: { ...mockAuthorToken, preloadedUsers: mockPreloadedUsers },
stubs: { Portal: true },
});
@@ -286,8 +286,8 @@ describe('AuthorToken', () => {
const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(2 + currentUserLength);
- expect(suggestions.at(0).text()).toBe(DEFAULT_NONE_ANY[0].text);
- expect(suggestions.at(1).text()).toBe(DEFAULT_NONE_ANY[1].text);
+ expect(suggestions.at(0).text()).toBe(OPTIONS_NONE_ANY[0].text);
+ expect(suggestions.at(1).text()).toBe(OPTIONS_NONE_ANY[1].text);
});
it('emits listeners in the base-token', () => {
@@ -308,8 +308,8 @@ describe('AuthorToken', () => {
active: true,
config: {
...mockAuthorToken,
- preloadedAuthors: mockPreloadedAuthors,
- defaultAuthors: [],
+ preloadedUsers: mockPreloadedUsers,
+ defaultUsers: [],
},
stubs: { Portal: true },
});
diff --git a/spec/frontend/vue_shared/components/group_select/group_select_spec.js b/spec/frontend/vue_shared/components/group_select/group_select_spec.js
index f959d2225fa..c10b32c6acc 100644
--- a/spec/frontend/vue_shared/components/group_select/group_select_spec.js
+++ b/spec/frontend/vue_shared/components/group_select/group_select_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
@@ -31,7 +31,7 @@ describe('GroupSelect', () => {
const inputId = 'inputId';
// Finders
- const findListbox = () => wrapper.findComponent(GlListbox);
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findInput = () => wrapper.findByTestId('input');
// Helpers
diff --git a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
new file mode 100644
index 00000000000..cb7262b15e3
--- /dev/null
+++ b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
@@ -0,0 +1,132 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlListbox } from '@gitlab/ui';
+import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue';
+
+describe('ListboxInput', () => {
+ let wrapper;
+
+ // Props
+ const name = 'name';
+ const defaultToggleText = 'defaultToggleText';
+ const items = [
+ {
+ text: 'Group 1',
+ options: [
+ { text: 'Item 1', value: '1' },
+ { text: 'Item 2', value: '2' },
+ ],
+ },
+ {
+ text: 'Group 2',
+ options: [{ text: 'Item 3', value: '3' }],
+ },
+ ];
+
+ // Finders
+ const findGlListbox = () => wrapper.findComponent(GlListbox);
+ const findInput = () => wrapper.find('input');
+
+ const createComponent = (propsData) => {
+ wrapper = shallowMount(ListboxInput, {
+ propsData: {
+ name,
+ defaultToggleText,
+ items,
+ ...propsData,
+ },
+ });
+ };
+
+ describe('input attributes', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets the input name', () => {
+ expect(findInput().attributes('name')).toBe(name);
+ });
+ });
+
+ describe('toggle text', () => {
+ it('uses the default toggle text while no value is selected', () => {
+ createComponent();
+
+ expect(findGlListbox().props('toggleText')).toBe(defaultToggleText);
+ });
+
+ it("uses the selected option's text as the toggle text", () => {
+ const selectedOption = items[0].options[0];
+ createComponent({ selected: selectedOption.value });
+
+ expect(findGlListbox().props('toggleText')).toBe(selectedOption.text);
+ });
+ });
+
+ describe('input value', () => {
+ const selectedOption = items[0].options[0];
+
+ beforeEach(() => {
+ createComponent({ selected: selectedOption.value });
+ jest.spyOn(findInput().element, 'dispatchEvent');
+ });
+
+ it("sets the listbox's and input's values", () => {
+ const { value } = selectedOption;
+
+ expect(findGlListbox().props('selected')).toBe(value);
+ expect(findInput().attributes('value')).toBe(value);
+ });
+
+ describe("when the listbox's value changes", () => {
+ const newSelectedOption = items[1].options[0];
+
+ beforeEach(() => {
+ findGlListbox().vm.$emit('select', newSelectedOption.value);
+ });
+
+ it('emits the `select` event', () => {
+ expect(wrapper.emitted('select')).toEqual([[newSelectedOption.value]]);
+ });
+ });
+ });
+
+ describe('search', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('passes all items to GlListbox by default', () => {
+ createComponent();
+ expect(findGlListbox().props('items')).toStrictEqual(items);
+ });
+
+ describe('with groups', () => {
+ beforeEach(() => {
+ createComponent();
+ findGlListbox().vm.$emit('search', '1');
+ });
+
+ it('passes only the items that match the search string', async () => {
+ expect(findGlListbox().props('items')).toStrictEqual([
+ {
+ text: 'Group 1',
+ options: [{ text: 'Item 1', value: '1' }],
+ },
+ ]);
+ });
+ });
+
+ describe('with flat items', () => {
+ beforeEach(() => {
+ createComponent({
+ items: items[0].options,
+ });
+ findGlListbox().vm.$emit('search', '1');
+ });
+
+ it('passes only the items that match the search string', async () => {
+ expect(findGlListbox().props('items')).toStrictEqual([{ text: 'Item 1', value: '1' }]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 50864a4bf25..285ea10c813 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -1,12 +1,14 @@
import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
-import $ from 'jquery';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownFieldHeader from '~/vue_shared/components/markdown/header.vue';
import MarkdownToolbar from '~/vue_shared/components/markdown/toolbar.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
const markdownPreviewPath = `${TEST_HOST}/preview`;
const markdownDocsPath = `${TEST_HOST}/docs`;
@@ -138,15 +140,13 @@ describe('Markdown field component', () => {
});
it('renders markdown preview and GFM', async () => {
- const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
-
previewLink = getPreviewLink();
previewLink.vm.$emit('click', { target: {} });
await axios.waitFor(markdownPreviewPath);
expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML);
- expect(renderGFMSpy).toHaveBeenCalled();
+ expect(renderGFM).toHaveBeenCalled();
});
it('calls video.pause() on comment input when isSubmitting is changed to true', async () => {
diff --git a/spec/frontend/vue_shared/components/markdown/field_view_spec.js b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
index be1d840dd29..176ccfc5a69 100644
--- a/spec/frontend/vue_shared/components/markdown/field_view_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
@@ -1,10 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import $ from 'jquery';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
describe('Markdown Field View component', () => {
- let renderGFMSpy;
let wrapper;
function createComponent() {
@@ -12,7 +13,6 @@ describe('Markdown Field View component', () => {
}
beforeEach(() => {
- renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
createComponent();
});
@@ -21,6 +21,6 @@ describe('Markdown Field View component', () => {
});
it('processes rendering with GFM', () => {
- expect(renderGFMSpy).toHaveBeenCalledTimes(1);
+ expect(renderGFM).toHaveBeenCalledTimes(1);
});
});
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 625e67c7cc1..5f416db2676 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -171,6 +171,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect.objectContaining({
renderMarkdown: expect.any(Function),
uploadsPath: window.uploads_path,
+ useBottomToolbar: false,
markdown: value,
}),
);
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
index 8edcb905096..2b311b75f85 100644
--- a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
+++ b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
@@ -1,5 +1,5 @@
import { GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import MarkdownDrawer, { cache } from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
import { getRenderedMarkdown } from '~/vue_shared/components/markdown_drawer/utils/fetch';
@@ -82,7 +82,10 @@ describe('MarkdownDrawer', () => {
contentTop.mockClear();
});
- it(`computes offsetTop ${hasNavbar ? 'with' : 'without'} .navbar-gitlab`, () => {
+ it(`computes offsetTop ${hasNavbar ? 'with' : 'without'} .navbar-gitlab`, async () => {
+ wrapper.vm.getDrawerTop();
+ await Vue.nextTick();
+
expect(findDrawer().attributes('headerheight')).toBe(`${navbarHeight}px`);
});
});
@@ -95,11 +98,11 @@ describe('MarkdownDrawer', () => {
renderGLFMSpy = jest.spyOn(MarkdownDrawer.methods, 'renderGLFM');
fetchMarkdownSpy = jest.spyOn(MarkdownDrawer.methods, 'fetchMarkdown');
global.document.querySelector = jest.fn(() => ({
- getBoundingClientRect: jest.fn(() => ({ bottom: 100 })),
dataset: {
page: 'test',
},
}));
+ contentTop.mockReturnValue(100);
createComponent();
await nextTick();
});
@@ -118,12 +121,28 @@ describe('MarkdownDrawer', () => {
expect(fetchMarkdownSpy).toHaveBeenCalledTimes(2);
});
- it('for open triggers renderGLFM', async () => {
+ it('triggers renderGLFM in openDrawer', async () => {
wrapper.vm.fetchMarkdown();
wrapper.vm.openDrawer();
await nextTick();
expect(renderGLFMSpy).toHaveBeenCalled();
});
+
+ it('triggers height calculation in openDrawer', async () => {
+ expect(findDrawer().attributes('headerheight')).toBe(`${0}px`);
+ wrapper.vm.fetchMarkdown();
+ wrapper.vm.openDrawer();
+ await nextTick();
+ expect(findDrawer().attributes('headerheight')).toBe(`${100}px`);
+ });
+
+ it('triggers height calculation in toggleDrawer', async () => {
+ expect(findDrawer().attributes('headerheight')).toBe(`${0}px`);
+ wrapper.vm.fetchMarkdown();
+ wrapper.vm.toggleDrawer();
+ await nextTick();
+ expect(findDrawer().attributes('headerheight')).toBe(`${100}px`);
+ });
});
describe('Markdown fetching', () => {
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
index ff07b2cf838..adcf57b76a4 100644
--- a/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
+++ b/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
@@ -20,9 +20,9 @@ describe('utils/fetch', () => {
});
describe.each`
- axiosMock | type | toExpect
- ${{ code: 200, res: { html: MOCK_HTML } }} | ${'success'} | ${MOCK_DRAWER_DATA}
- ${{ code: 500, res: null }} | ${'error'} | ${MOCK_DRAWER_DATA_ERROR}
+ axiosMock | type | toExpect
+ ${{ code: 200, res: MOCK_HTML }} | ${'success'} | ${MOCK_DRAWER_DATA}
+ ${{ code: 500, res: null }} | ${'error'} | ${MOCK_DRAWER_DATA_ERROR}
`('process markdown data', ({ axiosMock, type, toExpect }) => {
describe(`if api fetch responds with ${type}`, () => {
beforeEach(() => {
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 98b04ede943..559f9bcb1a8 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -1,10 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
-import $ from 'jquery';
import waitForPromises from 'helpers/wait_for_promises';
import createStore from '~/notes/stores';
import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
import axios from '~/lib/utils/axios_utils';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
describe('system note component', () => {
let vm;
@@ -75,11 +77,9 @@ describe('system note component', () => {
});
it('should renderGFM onMount', () => {
- const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
-
createComponent(props);
- expect(renderGFMSpy).toHaveBeenCalled();
+ expect(renderGFM).toHaveBeenCalled();
});
it('renders outdated code lines', async () => {
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/mocks/items_filters.json b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/mocks/items_filters.json
index b42ec42d8b8..e5678c9a956 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/mocks/items_filters.json
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/mocks/items_filters.json
@@ -1,14 +1,20 @@
[
- {
- "type": "assignee_username",
- "value": { "data": "root2" }
- },
- {
- "type": "author_username",
- "value": { "data": "root" }
- },
- {
- "type": "filtered-search-term",
- "value": { "data": "bar" }
+ {
+ "type": "assignee",
+ "value": {
+ "data": "root2"
}
- ] \ No newline at end of file
+ },
+ {
+ "type": "author",
+ "value": {
+ "data": "root"
+ }
+ },
+ {
+ "type": "filtered-search-term",
+ "value": {
+ "data": "bar"
+ }
+ }
+] \ No newline at end of file
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index c0c3c4a9729..86a63db0d9e 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -2,9 +2,15 @@ import { GlAlert, GlBadge, GlPagination, GlTabs, GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Tracking from '~/tracking';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ OPERATORS_IS,
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import PageWrapper from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import mockItems from './mocks/items.json';
import mockFilters from './mocks/items_filters.json';
@@ -166,7 +172,7 @@ describe('AlertManagementEmptyState', () => {
it('renders the filter set with the tokens according to the prop filterSearchTokens', () => {
mountComponent({
- props: { filterSearchTokens: ['assignee_username'] },
+ props: { filterSearchTokens: [TOKEN_TYPE_ASSIGNEE] },
});
expect(Filters().exists()).toBe(true);
@@ -287,26 +293,26 @@ describe('AlertManagementEmptyState', () => {
expect(Filters().props('searchInputPlaceholder')).toBe('Search or filter results…');
expect(Filters().props('tokens')).toEqual([
{
- type: 'author_username',
+ type: TOKEN_TYPE_AUTHOR,
icon: 'user',
- title: 'Author',
+ title: TOKEN_TITLE_AUTHOR,
unique: true,
symbol: '@',
- token: AuthorToken,
- operators: OPERATOR_IS_ONLY,
+ token: UserToken,
+ operators: OPERATORS_IS,
fetchPath: '/link',
- fetchAuthors: expect.any(Function),
+ fetchUsers: expect.any(Function),
},
{
- type: 'assignee_username',
+ type: TOKEN_TYPE_ASSIGNEE,
icon: 'user',
- title: 'Assignee',
+ title: TOKEN_TITLE_ASSIGNEE,
unique: true,
symbol: '@',
- token: AuthorToken,
- operators: OPERATOR_IS_ONLY,
+ token: UserToken,
+ operators: OPERATORS_IS,
fetchPath: '/link',
- fetchAuthors: expect.any(Function),
+ fetchUsers: expect.any(Function),
},
]);
expect(Filters().props('recentSearchesStorageKey')).toBe('items');
diff --git a/spec/frontend/vue_shared/components/registry/registry_search_spec.js b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
index fa7fabfaef6..591447a37c2 100644
--- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js
+++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
@@ -1,6 +1,6 @@
import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import component from '~/vue_shared/components/registry/registry_search.vue';
describe('Registry Search', () => {
diff --git a/spec/frontend/vue_shared/components/runner_instructions/mock_data.js b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
index bc1545014d7..79cacadd6af 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
@@ -1,119 +1,5 @@
-export const mockGraphqlRunnerPlatforms = {
- data: {
- runnerPlatforms: {
- nodes: [
- {
- name: 'linux',
- humanReadableName: 'Linux',
- architectures: {
- nodes: [
- {
- name: 'amd64',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64',
- __typename: 'RunnerArchitecture',
- },
- {
- name: '386',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386',
- __typename: 'RunnerArchitecture',
- },
- {
- name: 'arm',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm',
- __typename: 'RunnerArchitecture',
- },
- {
- name: 'arm64',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64',
- __typename: 'RunnerArchitecture',
- },
- ],
- __typename: 'RunnerArchitectureConnection',
- },
- __typename: 'RunnerPlatform',
- },
- {
- name: 'osx',
- humanReadableName: 'macOS',
- architectures: {
- nodes: [
- {
- name: 'amd64',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64',
- __typename: 'RunnerArchitecture',
- },
- ],
- __typename: 'RunnerArchitectureConnection',
- },
- __typename: 'RunnerPlatform',
- },
- {
- name: 'windows',
- humanReadableName: 'Windows',
- architectures: {
- nodes: [
- {
- name: 'amd64',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe',
- __typename: 'RunnerArchitecture',
- },
- {
- name: '386',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe',
- __typename: 'RunnerArchitecture',
- },
- ],
- __typename: 'RunnerArchitectureConnection',
- },
- __typename: 'RunnerPlatform',
- },
- {
- name: 'docker',
- humanReadableName: 'Docker',
- architectures: null,
- __typename: 'RunnerPlatform',
- },
- {
- name: 'kubernetes',
- humanReadableName: 'Kubernetes',
- architectures: null,
- __typename: 'RunnerPlatform',
- },
- ],
- __typename: 'RunnerPlatformConnection',
- },
- project: { id: 'gid://gitlab/Project/1', __typename: 'Project' },
- group: null,
- },
-};
+import mockGraphqlRunnerPlatforms from 'test_fixtures/graphql/runner_instructions/get_runner_platforms.query.graphql.json';
+import mockGraphqlInstructions from 'test_fixtures/graphql/runner_instructions/get_runner_setup.query.graphql.json';
+import mockGraphqlInstructionsWindows from 'test_fixtures/graphql/runner_instructions/get_runner_setup.query.graphql.windows.json';
-export const mockGraphqlInstructions = {
- data: {
- runnerSetup: {
- installInstructions:
- '# Install and run as service\nsudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner\nsudo gitlab-runner start',
- registerInstructions:
- 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token $REGISTRATION_TOKEN',
- __typename: 'RunnerSetup',
- },
- },
-};
-
-export const mockGraphqlInstructionsWindows = {
- data: {
- runnerSetup: {
- installInstructions:
- '# Windows runner, then run\n.gitlab-runner.exe install\n.gitlab-runner.exe start',
- registerInstructions:
- './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token $REGISTRATION_TOKEN',
- __typename: 'RunnerSetup',
- },
- },
-};
+export { mockGraphqlRunnerPlatforms, mockGraphqlInstructions, mockGraphqlInstructionsWindows };
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index 7c5fc63856a..ae9157591c5 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -113,10 +113,7 @@ describe('RunnerInstructionsModal component', () => {
});
describe('should display default instructions', () => {
- const {
- installInstructions,
- registerInstructions,
- } = mockGraphqlInstructions.data.runnerSetup;
+ const { installInstructions } = mockGraphqlInstructions.data.runnerSetup;
it('runner instructions are requested', () => {
expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
@@ -128,53 +125,16 @@ describe('RunnerInstructionsModal component', () => {
it('binary instructions are shown', async () => {
const instructions = findBinaryInstructions().text();
- expect(instructions).toBe(installInstructions);
+ expect(instructions).toBe(installInstructions.trim());
});
it('register command is shown with a replaced token', async () => {
const command = findRegisterCommand().text();
expect(command).toBe(
- 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
+ 'sudo gitlab-runner register --url http://localhost/ --registration-token MY_TOKEN',
);
});
-
- describe('when a register token is not shown', () => {
- beforeEach(async () => {
- createComponent({ props: { registrationToken: undefined } });
- await waitForPromises();
- });
-
- it('register command is shown without a defined registration token', () => {
- const instructions = findRegisterCommand().text();
-
- expect(instructions).toBe(registerInstructions);
- });
- });
-
- describe('when providing a defaultPlatformName', () => {
- beforeEach(async () => {
- createComponent({ props: { defaultPlatformName: 'osx' } });
- await waitForPromises();
- });
-
- it('runner instructions for the default selected platform are requested', () => {
- expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
- platform: 'osx',
- architecture: 'amd64',
- });
- });
-
- it('sets the focus on the default selected platform', () => {
- const findOsxPlatformButton = () => wrapper.findComponent({ ref: 'osx' });
-
- findOsxPlatformButton().element.focus = jest.fn();
-
- findModal().vm.$emit('shown');
-
- expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
- });
- });
});
describe('after a platform and architecture are selected', () => {
@@ -207,14 +167,14 @@ describe('RunnerInstructionsModal component', () => {
it('other binary instructions are shown', () => {
const instructions = findBinaryInstructions().text();
- expect(instructions).toBe(installInstructions);
+ expect(instructions).toBe(installInstructions.trim());
});
it('register command is shown', () => {
const command = findRegisterCommand().text();
expect(command).toBe(
- './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
+ './gitlab-runner.exe register --url http://localhost/ --registration-token MY_TOKEN',
);
});
@@ -246,6 +206,43 @@ describe('RunnerInstructionsModal component', () => {
});
});
+ describe('when a register token is not known', () => {
+ beforeEach(async () => {
+ createComponent({ props: { registrationToken: undefined } });
+ await waitForPromises();
+ });
+
+ it('register command is shown without a defined registration token', () => {
+ const instructions = findRegisterCommand().text();
+
+ expect(instructions).toBe(mockGraphqlInstructions.data.runnerSetup.registerInstructions);
+ });
+ });
+
+ describe('with a defaultPlatformName', () => {
+ beforeEach(async () => {
+ createComponent({ props: { defaultPlatformName: 'osx' } });
+ await waitForPromises();
+ });
+
+ it('runner instructions for the default selected platform are requested', () => {
+ expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
+ platform: 'osx',
+ architecture: 'amd64',
+ });
+ });
+
+ it('sets the focus on the default selected platform', () => {
+ const findOsxPlatformButton = () => wrapper.findComponent({ ref: 'osx' });
+
+ findOsxPlatformButton().element.focus = jest.fn();
+
+ findModal().vm.$emit('shown');
+
+ expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
+ });
+ });
+
describe('when the modal is not shown', () => {
beforeEach(async () => {
createComponent({ shown: false });
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
index d720574ce6d..657bd59dac6 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
@@ -1,10 +1,16 @@
+import { nextTick } from 'vue';
import { GlIntersectionObserver } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
-import { scrollToElement } from '~/lib/utils/common_utils';
+import LineHighlighter from '~/blob/line_highlighter';
-jest.mock('~/lib/utils/common_utils');
+const lineHighlighter = new LineHighlighter();
+jest.mock('~/blob/line_highlighter', () =>
+ jest.fn().mockReturnValue({
+ highlightHash: jest.fn(),
+ }),
+);
const DEFAULT_PROPS = {
chunkIndex: 2,
@@ -104,12 +110,14 @@ describe('Chunk component', () => {
});
it('does not scroll to route hash if last chunk is not loaded', () => {
- expect(scrollToElement).not.toHaveBeenCalled();
+ expect(LineHighlighter).not.toHaveBeenCalled();
});
- it('scrolls to route hash if last chunk is loaded', () => {
+ it('scrolls to route hash if last chunk is loaded', async () => {
createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
- expect(scrollToElement).toHaveBeenCalledWith(hash, { behavior: 'auto' });
+ await nextTick();
+ expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
});
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
index a7b55d7332f..4d38e8ef25d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
@@ -4,15 +4,16 @@ import gemspecLinker from '~/vue_shared/components/source_viewer/plugins/utils/g
import gemfileLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker';
import podspecJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker';
import composerJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/composer_json_linker';
+import goSumLinker from '~/vue_shared/components/source_viewer/plugins/utils/go_sum_linker';
import linkDependencies from '~/vue_shared/components/source_viewer/plugins/link_dependencies';
import {
PACKAGE_JSON_FILE_TYPE,
- PACKAGE_JSON_CONTENT,
GEMSPEC_FILE_TYPE,
GODEPS_JSON_FILE_TYPE,
GEMFILE_FILE_TYPE,
PODSPEC_JSON_FILE_TYPE,
COMPOSER_JSON_FILE_TYPE,
+ GO_SUM_FILE_TYPE,
} from './mock_data';
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/package_json_linker');
@@ -21,37 +22,31 @@ jest.mock('~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linke
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker');
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker');
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/composer_json_linker');
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/go_sum_linker');
describe('Highlight.js plugin for linking dependencies', () => {
const hljsResultMock = { value: 'test' };
- it('calls packageJsonLinker for package_json file types', () => {
- linkDependencies(hljsResultMock, PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT);
- expect(packageJsonLinker).toHaveBeenCalled();
- });
-
- it('calls gemspecLinker for gemspec file types', () => {
- linkDependencies(hljsResultMock, GEMSPEC_FILE_TYPE);
- expect(gemspecLinker).toHaveBeenCalled();
- });
-
- it('calls godepsJsonLinker for godeps_json file types', () => {
- linkDependencies(hljsResultMock, GODEPS_JSON_FILE_TYPE);
- expect(godepsJsonLinker).toHaveBeenCalled();
- });
-
- it('calls gemfileLinker for gemfile file types', () => {
- linkDependencies(hljsResultMock, GEMFILE_FILE_TYPE);
- expect(gemfileLinker).toHaveBeenCalled();
- });
-
- it('calls podspecJsonLinker for podspec_json file types', () => {
- linkDependencies(hljsResultMock, PODSPEC_JSON_FILE_TYPE);
- expect(podspecJsonLinker).toHaveBeenCalled();
- });
-
- it('calls composerJsonLinker for composer_json file types', () => {
- linkDependencies(hljsResultMock, COMPOSER_JSON_FILE_TYPE);
- expect(composerJsonLinker).toHaveBeenCalled();
+ describe.each`
+ fileType | linker
+ ${PACKAGE_JSON_FILE_TYPE} | ${packageJsonLinker}
+ ${GEMSPEC_FILE_TYPE} | ${gemspecLinker}
+ ${GODEPS_JSON_FILE_TYPE} | ${godepsJsonLinker}
+ ${GEMFILE_FILE_TYPE} | ${gemfileLinker}
+ ${PODSPEC_JSON_FILE_TYPE} | ${podspecJsonLinker}
+ ${COMPOSER_JSON_FILE_TYPE} | ${composerJsonLinker}
+ ${GO_SUM_FILE_TYPE} | ${goSumLinker}
+ `('$fileType file type', ({ fileType, linker }) => {
+ it('calls the correct linker', () => {
+ linkDependencies(hljsResultMock, fileType);
+ expect(linker).toHaveBeenCalled();
+ });
+
+ it('does not call the linker for non-matching file types', () => {
+ const unknownFileType = 'unknown';
+
+ linkDependencies(hljsResultMock, unknownFileType);
+ expect(linker).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
index 5455479ec71..631baf19a2d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
@@ -32,3 +32,5 @@ export const PODSPEC_JSON_CONTENT = `{
}`;
export const COMPOSER_JSON_FILE_TYPE = 'composer_json';
+
+export const GO_SUM_FILE_TYPE = 'go_sum';
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/go_sum_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/go_sum_linker_spec.js
new file mode 100644
index 00000000000..cc3ee41523f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/go_sum_linker_spec.js
@@ -0,0 +1,14 @@
+import goSumLinker from '~/vue_shared/components/source_viewer/plugins/utils/go_sum_linker';
+
+describe('Highlight.js plugin for linking go.sum dependencies', () => {
+ it('mutates the input value by wrapping dependencies and tags in anchors', () => {
+ const inputValue =
+ '<span class="">cloud.google.com/Go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=</span>';
+ const outputValue =
+ '<span class=""><a href="https://pkg.go.dev/cloud.google.com/go/bigquery" target="_blank" rel="nofollow noreferrer noopener">cloud.google.com/Go/bigquery</a> v1.0.1/go.mod h1:<a href="https://sum.golang.org/lookup/cloud.google.com/go/bigquery@v1.0.1" target="_blank" rel="nofollow noreferrer noopener">i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=</a></span>';
+ const hljsResultMock = { value: inputValue };
+
+ const output = goSumLinker(hljsResultMock);
+ expect(output).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index 4188adc72a1..874796f653a 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -10,7 +10,7 @@ import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_
import { IssuableType } from '~/issues/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
-import getIssueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
+import getIssueParticipantsQuery from '~/sidebar/queries/get_issue_participants.query.graphql';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import {
searchResponse,
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 a0b868d1d52..3b0f0fe6e73 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -1,13 +1,20 @@
-import { GlModal } from '@gitlab/ui';
+import { GlButton, GlModal, GlPopover } from '@gitlab/ui';
import { nextTick } from 'vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import WebIdeLink, { i18n } from '~/vue_shared/components/web_ide_link.vue';
+import WebIdeLink, {
+ i18n,
+ PREFERRED_EDITOR_RESET_KEY,
+ PREFERRED_EDITOR_KEY,
+ KEY_WEB_IDE,
+} from '~/vue_shared/components/web_ide_link.vue';
import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
const TEST_EDIT_URL = '/gitlab-test/test/-/edit/main/';
const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/main/-/';
@@ -79,9 +86,18 @@ const ACTION_PIPELINE_EDITOR = {
};
describe('Web IDE link component', () => {
+ useLocalStorageSpy();
+
let wrapper;
- function createComponent(props, mountFn = shallowMountExtended) {
+ function createComponent(
+ props,
+ {
+ mountFn = shallowMountExtended,
+ glFeatures = {},
+ userCalloutDismisserSlotProps = { dismiss: jest.fn() },
+ } = {},
+ ) {
wrapper = mountFn(WebIdeLink, {
propsData: {
editUrl: TEST_EDIT_URL,
@@ -91,6 +107,9 @@ describe('Web IDE link component', () => {
forkPath,
...props,
},
+ provide: {
+ glFeatures,
+ },
stubs: {
GlModal: stubComponent(GlModal, {
template: `
@@ -100,10 +119,19 @@ describe('Web IDE link component', () => {
<slot name="modal-footer"></slot>
</div>`,
}),
+ UserCalloutDismisser: stubComponent(UserCalloutDismisser, {
+ render() {
+ return this.$scopedSlots.default(userCalloutDismisserSlotProps);
+ },
+ }),
},
});
}
+ beforeEach(() => {
+ localStorage.setItem(PREFERRED_EDITOR_RESET_KEY, 'true');
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -112,6 +140,8 @@ describe('Web IDE link component', () => {
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const findModal = () => wrapper.findComponent(GlModal);
const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal);
+ const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
+ const findNewWebIdeCalloutPopover = () => wrapper.findComponent(GlPopover);
it.each([
{
@@ -322,9 +352,9 @@ describe('Web IDE link component', () => {
});
it.each(testActions)('opens the modal when the button is clicked', async ({ props }) => {
- createComponent({ ...props, needsToFork: true }, mountExtended);
+ createComponent({ ...props, needsToFork: true }, { mountFn: mountExtended });
- await findActionsButton().trigger('click');
+ await findActionsButton().findComponent(GlButton).trigger('click');
expect(findForkConfirmModal().props()).toEqual({
visible: true,
@@ -377,7 +407,7 @@ describe('Web IDE link component', () => {
gitpodEnabled: false,
gitpodText,
},
- mountExtended,
+ { mountFn: mountExtended },
);
findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key);
@@ -401,4 +431,178 @@ describe('Web IDE link component', () => {
expect(findModal().exists()).toBe(false);
});
});
+
+ describe('Web IDE callout', () => {
+ describe('vscode_web_ide feature flag is enabled and the edit button is not shown', () => {
+ let dismiss;
+
+ beforeEach(() => {
+ dismiss = jest.fn();
+ createComponent(
+ {
+ showEditButton: false,
+ },
+ {
+ glFeatures: { vscodeWebIde: true },
+ userCalloutDismisserSlotProps: { dismiss },
+ },
+ );
+ });
+ it('does not skip the user_callout_dismisser query', () => {
+ expect(findUserCalloutDismisser().props()).toEqual(
+ expect.objectContaining({
+ skipQuery: false,
+ featureName: 'vscode_web_ide_callout',
+ }),
+ );
+ });
+
+ it('mounts new web ide callout popover', () => {
+ expect(findNewWebIdeCalloutPopover().props()).toEqual(
+ expect.objectContaining({
+ showCloseButton: '',
+ target: 'web-ide-link',
+ triggers: 'manual',
+ boundaryPadding: 80,
+ }),
+ );
+ });
+
+ describe.each`
+ calloutStatus | shouldShowCallout | popoverVisibility | tooltipVisibility
+ ${'show'} | ${true} | ${true} | ${false}
+ ${'hide'} | ${false} | ${false} | ${true}
+ `(
+ 'when should $calloutStatus web ide callout',
+ ({ shouldShowCallout, popoverVisibility, tooltipVisibility }) => {
+ beforeEach(() => {
+ createComponent(
+ {
+ showEditButton: false,
+ },
+ {
+ glFeatures: { vscodeWebIde: true },
+ userCalloutDismisserSlotProps: { shouldShowCallout, dismiss },
+ },
+ );
+ });
+
+ it(`popover visibility = ${popoverVisibility}`, () => {
+ expect(findNewWebIdeCalloutPopover().props().show).toBe(popoverVisibility);
+ });
+
+ it(`action button tooltip visibility = ${tooltipVisibility}`, () => {
+ expect(findActionsButton().props().showActionTooltip).toBe(tooltipVisibility);
+ });
+ },
+ );
+
+ it('dismisses the callout when popover close button is clicked', () => {
+ findNewWebIdeCalloutPopover().vm.$emit('close-button-clicked');
+
+ expect(dismiss).toHaveBeenCalled();
+ });
+
+ it('dismisses the callout when action button is clicked', () => {
+ findActionsButton().vm.$emit('actionClicked');
+
+ expect(dismiss).toHaveBeenCalled();
+ });
+ });
+
+ describe.each`
+ featureFlag | showEditButton
+ ${false} | ${true}
+ ${true} | ${false}
+ ${false} | ${false}
+ `(
+ 'when vscode_web_ide=$featureFlag and showEditButton = $showEditButton',
+ ({ vscodeWebIde, showEditButton }) => {
+ let dismiss;
+
+ beforeEach(() => {
+ dismiss = jest.fn();
+
+ createComponent(
+ {
+ showEditButton,
+ },
+ { glFeatures: { vscodeWebIde }, userCalloutDismisserSlotProps: { dismiss } },
+ );
+ });
+
+ it('skips the user_callout_dismisser query', () => {
+ expect(findUserCalloutDismisser().props().skipQuery).toBe(true);
+ });
+
+ it('displays actions button tooltip', () => {
+ expect(findActionsButton().props().showActionTooltip).toBe(true);
+ });
+
+ it('mounts new web ide callout popover', () => {
+ expect(findNewWebIdeCalloutPopover().exists()).toBe(false);
+ });
+
+ it('does not dismiss the callout when action button is clicked', () => {
+ findActionsButton().vm.$emit('actionClicked');
+
+ expect(dismiss).not.toHaveBeenCalled();
+ });
+ },
+ );
+ });
+
+ describe('when vscode_web_ide feature flag is enabled', () => {
+ describe('when is not showing edit button', () => {
+ describe(`when ${PREFERRED_EDITOR_RESET_KEY} is unset`, () => {
+ beforeEach(() => {
+ localStorage.setItem.mockReset();
+ localStorage.getItem.mockReturnValueOnce(null);
+ createComponent({ showEditButton: false }, { glFeatures: { vscodeWebIde: true } });
+ });
+
+ it(`sets ${PREFERRED_EDITOR_KEY} local storage key to ${KEY_WEB_IDE}`, () => {
+ expect(localStorage.getItem).toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY);
+ expect(localStorage.setItem).toHaveBeenCalledWith(PREFERRED_EDITOR_KEY, KEY_WEB_IDE);
+ });
+
+ it(`sets ${PREFERRED_EDITOR_RESET_KEY} local storage key to true`, () => {
+ expect(localStorage.setItem).toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY, true);
+ });
+
+ it(`selects ${KEY_WEB_IDE} as the preferred editor`, () => {
+ expect(findActionsButton().props().selectedKey).toBe(KEY_WEB_IDE);
+ });
+ });
+
+ describe(`when ${PREFERRED_EDITOR_RESET_KEY} is set to true`, () => {
+ beforeEach(() => {
+ localStorage.setItem.mockReset();
+ localStorage.getItem.mockReturnValueOnce('true');
+ createComponent({ showEditButton: false }, { glFeatures: { vscodeWebIde: true } });
+ });
+
+ it(`does not update the persisted preferred editor`, () => {
+ expect(localStorage.getItem).toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY);
+ expect(localStorage.setItem).not.toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY);
+ });
+ });
+ });
+
+ describe('when is showing the edit button', () => {
+ it(`does not try to reset the ${PREFERRED_EDITOR_KEY}`, () => {
+ createComponent({ showEditButton: true }, { glFeatures: { vscodeWebIde: true } });
+
+ expect(localStorage.getItem).not.toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY);
+ });
+ });
+ });
+
+ describe('when vscode_web_ide feature flag is disabled', () => {
+ it(`does not try to reset the ${PREFERRED_EDITOR_KEY}`, () => {
+ createComponent({}, { glFeatures: { vscodeWebIde: false } });
+
+ expect(localStorage.getItem).not.toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
index f98e7a678f4..ff21b3bc356 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import IssuableForm from '~/vue_shared/issuable/create/components/issuable_form.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
+import LabelsSelect from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue';
const createComponent = ({
descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown',
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js
new file mode 100644
index 00000000000..76b6efa15b6
--- /dev/null
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js
@@ -0,0 +1,141 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import {
+ mockRegularLabel,
+ mockScopedLabel,
+} from 'jest/sidebar/components/labels/labels_select_widget/mock_data';
+import IssuableLabelSelector from '~/vue_shared/issuable/create/components/issuable_label_selector.vue';
+import LabelsSelect from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
+import {
+ DropdownVariant,
+ LabelType,
+} from '~/sidebar/components/labels/labels_select_widget/constants';
+import { WorkspaceType } from '~/issues/constants';
+import { __ } from '~/locale';
+
+const allowLabelRemove = true;
+const attrWorkspacePath = '/workspace-path';
+const fieldName = 'field_name[]';
+const fullPath = '/full-path';
+const labelsFilterBasePath = '/labels-filter-base-path';
+const initialLabels = [];
+const issuableType = 'issue';
+const labelType = LabelType.project;
+const variant = DropdownVariant.Embedded;
+const workspaceType = WorkspaceType.project;
+
+describe('IssuableLabelSelector', () => {
+ let wrapper;
+
+ const findTitle = () => wrapper.find('label').text().replace(/\s+/, ' ');
+ const findLabelIcon = () => wrapper.findComponent(GlIcon);
+ const findAllHiddenInputs = () => wrapper.findAll('input[type="hidden"]');
+ const findLabelSelector = () => wrapper.findComponent(LabelsSelect);
+
+ const createComponent = (injectedProps = {}) => {
+ return shallowMount(IssuableLabelSelector, {
+ provide: {
+ allowLabelRemove,
+ attrWorkspacePath,
+ fieldName,
+ fullPath,
+ labelsFilterBasePath,
+ initialLabels,
+ issuableType,
+ labelType,
+ variant,
+ workspaceType,
+ ...injectedProps,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const expectTitleWithCount = (count) => {
+ const title = findTitle();
+
+ expect(title).toContain(__('Labels'));
+ expect(title).toContain(count.toString());
+ };
+
+ describe('by default', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('has the selected labels count', () => {
+ expectTitleWithCount(0);
+ expect(findLabelIcon().props('name')).toBe('labels');
+ });
+
+ it('has the label selector', () => {
+ expect(findLabelSelector().props()).toMatchObject({
+ allowLabelRemove,
+ allowMultiselect: true,
+ showEmbeddedLabelsList: true,
+ fullPath,
+ attrWorkspacePath,
+ labelsFilterBasePath,
+ dropdownButtonText: __('Select label'),
+ labelsListTitle: __('Select label'),
+ footerCreateLabelTitle: __('Create project label'),
+ footerManageLabelTitle: __('Manage project labels'),
+ variant,
+ workspaceType,
+ labelCreateType: labelType,
+ selectedLabels: initialLabels,
+ });
+
+ expect(findLabelSelector().text()).toBe(__('None'));
+ });
+ });
+
+ it('passing initial labels applies them to the form', () => {
+ wrapper = createComponent({ initialLabels: [mockRegularLabel, mockScopedLabel] });
+
+ expectTitleWithCount(2);
+ expect(findLabelSelector().props('selectedLabels')).toStrictEqual([
+ mockRegularLabel,
+ mockScopedLabel,
+ ]);
+ expect(findAllHiddenInputs().wrappers.map((input) => input.element.value)).toStrictEqual([
+ `${mockRegularLabel.id}`,
+ `${mockScopedLabel.id}`,
+ ]);
+ });
+
+ it('updates the selected labels on the `updateSelectedLabels` event', async () => {
+ wrapper = createComponent();
+
+ expectTitleWithCount(0);
+ expect(findLabelSelector().props('selectedLabels')).toStrictEqual([]);
+ expect(findAllHiddenInputs()).toHaveLength(0);
+
+ await findLabelSelector().vm.$emit('updateSelectedLabels', { labels: [mockRegularLabel] });
+
+ expectTitleWithCount(1);
+ expect(findLabelSelector().props('selectedLabels')).toStrictEqual([mockRegularLabel]);
+ expect(findAllHiddenInputs().wrappers.map((input) => input.element.value)).toStrictEqual([
+ `${mockRegularLabel.id}`,
+ ]);
+ });
+
+ it('updates the selected labels on the `onLabelRemove` event', async () => {
+ wrapper = createComponent({ initialLabels: [mockRegularLabel] });
+
+ expectTitleWithCount(1);
+ expect(findLabelSelector().props('selectedLabels')).toStrictEqual([mockRegularLabel]);
+ expect(findAllHiddenInputs().wrappers.map((input) => input.element.value)).toStrictEqual([
+ `${mockRegularLabel.id}`,
+ ]);
+
+ await findLabelSelector().vm.$emit('onLabelRemove', mockRegularLabel.id);
+
+ expectTitleWithCount(0);
+ expect(findLabelSelector().props('selectedLabels')).toStrictEqual([]);
+ expect(findAllHiddenInputs()).toHaveLength(0);
+ });
+});
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index e1c6020686c..2fac004875a 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -225,7 +225,7 @@ describe('IssuableItem', () => {
},
});
- expect(wrapper.findByTestId('issuable-discussions').exists()).toBe(returnValue);
+ expect(wrapper.findByTestId('issuable-comments').exists()).toBe(returnValue);
},
);
});
@@ -489,7 +489,7 @@ describe('IssuableItem', () => {
it('renders discussions count', () => {
wrapper = createComponent();
- const discussionsEl = wrapper.find('[data-testid="issuable-discussions"]');
+ const discussionsEl = wrapper.findByTestId('issuable-comments');
expect(discussionsEl.exists()).toBe(true);
expect(discussionsEl.findComponent(GlLink).attributes()).toMatchObject({
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js
index f2211e5b2bb..ea58cc2baf5 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js
@@ -1,10 +1,12 @@
import { shallowMount } from '@vue/test-utils';
-import $ from 'jquery';
import IssuableDescription from '~/vue_shared/issuable/show/components/issuable_description.vue';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { mockIssuable } from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
const createComponent = ({
issuable = mockIssuable,
enableTaskList = true,
@@ -16,11 +18,9 @@ const createComponent = ({
});
describe('IssuableDescription', () => {
- let renderGFMSpy;
let wrapper;
beforeEach(() => {
- renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
wrapper = createComponent();
});
@@ -30,17 +30,7 @@ describe('IssuableDescription', () => {
describe('mounted', () => {
it('calls `renderGFM`', () => {
- expect(renderGFMSpy).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('methods', () => {
- describe('renderGFM', () => {
- it('calls `renderGFM` on container element', () => {
- wrapper.vm.renderGFM();
-
- expect(renderGFMSpy).toHaveBeenCalled();
- });
+ expect(renderGFM).toHaveBeenCalledTimes(1);
});
});
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 3dbff024a6b..aec0f84cb82 100644
--- a/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
+++ b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
@@ -141,7 +141,7 @@ exports[`Webhook push events form editor component Different push events rules w
class="form-text text-muted custom-control"
>
<gl-sprintf-stub
- message="Regex such as %{REGEX_CODE} is supported."
+ message="Regular expressions such as %{REGEX_CODE} are supported."
/>
</p>
</gl-form-radio-group-stub>
@@ -367,7 +367,7 @@ exports[`Webhook push events form editor component Different push events rules w
class="form-text text-muted custom-control"
>
<gl-sprintf-stub
- message="Regex such as %{REGEX_CODE} is supported."
+ message="Regular expressions such as %{REGEX_CODE} are supported."
/>
</p>
</gl-form-radio-group-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
new file mode 100644
index 00000000000..3e3b8bf65b2
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/system_note_spec.js
@@ -0,0 +1,111 @@
+import { GlIcon } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import WorkItemSystemNote from '~/work_items/components/notes/system_note.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+import axios from '~/lib/utils/axios_utils';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+describe('system note component', () => {
+ let wrapper;
+ let props;
+ let mock;
+
+ const findTimelineIcon = () => wrapper.findComponent(GlIcon);
+ const findSystemNoteMessage = () => wrapper.findComponent(NoteHeader);
+ const findOutdatedLineButton = () =>
+ wrapper.findComponent('[data-testid="outdated-lines-change-btn"]');
+ const findOutdatedLines = () => wrapper.findComponent('[data-testid="outdated-lines"]');
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(WorkItemSystemNote, {
+ propsData,
+ slots: {
+ 'extra-controls':
+ '<gl-button data-testid="outdated-lines-change-btn">Compare with last version</gl-button>',
+ },
+ });
+ };
+
+ beforeEach(() => {
+ props = {
+ note: {
+ id: '1424',
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatarUrl: 'path',
+ path: '/root',
+ },
+ bodyHtml: '<p dir="auto">closed</p>',
+ systemNoteIconName: 'status_closed',
+ createdAt: '2017-08-02T10:51:58.559Z',
+ },
+ };
+
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should render a list item with correct id', () => {
+ createComponent(props);
+
+ expect(wrapper.attributes('id')).toBe(`note_${props.note.id}`);
+ });
+
+ // Note: The test case below is to handle a use case related to vuex store but since this does not
+ // have a vuex store , disabling it now will be fixing it in the next iteration
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('should render target class is note is target note', () => {
+ createComponent(props);
+
+ expect(wrapper.classes()).toContain('target');
+ });
+
+ it('should render svg icon', () => {
+ createComponent(props);
+
+ expect(findTimelineIcon().exists()).toBe(true);
+ });
+
+ // Redcarpet Markdown renderer wraps text in `<p>` tags
+ // we need to strip them because they break layout of commit lists in system notes:
+ // https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
+ it('removes wrapping paragraph from note HTML', () => {
+ createComponent(props);
+
+ expect(findSystemNoteMessage().html()).toContain('<span>closed</span>');
+ });
+
+ it('should renderGFM onMount', () => {
+ createComponent(props);
+
+ expect(renderGFM).toHaveBeenCalled();
+ });
+
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('renders outdated code lines', async () => {
+ mock
+ .onGet('/outdated_line_change_path')
+ .reply(200, [
+ { rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
+ ]);
+
+ createComponent({
+ note: { ...props.note, outdated_line_change_path: '/outdated_line_change_path' },
+ });
+
+ await findOutdatedLineButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(findOutdatedLines().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index 7367212e49f..e85f62b881d 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -435,6 +435,20 @@ describe('WorkItemAssignees component', () => {
expect(findTokenSelector().props('containerClass')).toBe('gl-shadow-none!');
});
+
+ it('calls the mutation for updating assignees with the correct input', async () => {
+ findTokenSelector().vm.$emit('input', [mockAssignees[1]]);
+ await waitForPromises();
+
+ expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ assigneesWidget: {
+ assigneeIds: [mockAssignees[1].id],
+ },
+ id: 'gid://gitlab/WorkItem/1',
+ },
+ });
+ });
});
describe('tracking', () => {
diff --git a/spec/frontend/work_items/components/work_item_description_rendered_spec.js b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
index 01ab7824975..0ab2546440b 100644
--- a/spec/frontend/work_items/components/work_item_description_rendered_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
@@ -1,9 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import $ from 'jquery';
import { nextTick } from 'vue';
import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { descriptionTextWithCheckboxes, descriptionHtmlWithCheckboxes } from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
describe('WorkItemDescription', () => {
let wrapper;
@@ -32,13 +34,11 @@ describe('WorkItemDescription', () => {
});
it('renders gfm', async () => {
- const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
-
createComponent();
await nextTick();
- expect(renderGFMSpy).toHaveBeenCalled();
+ expect(renderGFM).toHaveBeenCalled();
});
describe('with checkboxes', () => {
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 c79b049442d..05476ef5ca0 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -38,7 +38,7 @@ describe('WorkItemDescription', () => {
const subscriptionHandler = jest.fn().mockResolvedValue(workItemDescriptionSubscriptionResponse);
const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
let workItemResponseHandler;
- let workItemsMvc2;
+ let workItemsMvc;
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
@@ -46,7 +46,7 @@ describe('WorkItemDescription', () => {
const findEditedAt = () => wrapper.findComponent(EditedAt);
const editDescription = (newText) => {
- if (workItemsMvc2) {
+ if (workItemsMvc) {
return findMarkdownEditor().vm.$emit('input', newText);
}
return wrapper.find('textarea').setValue(newText);
@@ -60,6 +60,7 @@ describe('WorkItemDescription', () => {
canUpdate = true,
workItemResponse = workItemResponseFactory({ canUpdate }),
isEditing = false,
+ queryVariables = { id: workItemId },
fetchByIid = false,
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
@@ -75,14 +76,12 @@ describe('WorkItemDescription', () => {
propsData: {
workItemId: id,
fullPath: 'test-project-path',
- queryVariables: {
- id: workItemId,
- },
+ queryVariables,
fetchByIid,
},
provide: {
glFeatures: {
- workItemsMvc2,
+ workItemsMvc,
},
},
stubs: {
@@ -104,11 +103,21 @@ describe('WorkItemDescription', () => {
});
describe.each([true, false])(
- 'editing description with workItemsMvc2 %workItemsMvc2Enabled',
- (workItemsMvc2Enabled) => {
+ 'editing description with workItemsMvc %workItemsMvcEnabled',
+ (workItemsMvcEnabled) => {
beforeEach(() => {
beforeEach(() => {
- workItemsMvc2 = workItemsMvc2Enabled;
+ workItemsMvc = workItemsMvcEnabled;
+ });
+ });
+
+ it('has a subscription', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(subscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemQueryResponse.data.workItem.id,
});
});
@@ -275,6 +284,13 @@ describe('WorkItemDescription', () => {
expect(workItemResponseHandler).not.toHaveBeenCalled();
expect(workItemByIidResponseHandler).toHaveBeenCalled();
});
+
+ it('skips calling the handlers when missing the needed queryVariables', async () => {
+ createComponent({ queryVariables: {}, fetchByIid: false });
+ await waitForPromises();
+
+ expect(workItemResponseHandler).not.toHaveBeenCalled();
+ });
},
);
});
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index 4029e47c390..686641800b3 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -86,7 +86,7 @@ describe('WorkItemDetailModal component', () => {
isModal: true,
workItemId: defaultPropsData.workItemId,
workItemParentId: defaultPropsData.issueGid,
- iid: null,
+ workItemIid: null,
});
});
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 26777b57797..bbab45c7055 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -11,7 +11,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
@@ -21,7 +21,7 @@ import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
-import WorkItemInformation from '~/work_items/components/work_item_information.vue';
+import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
@@ -31,7 +31,6 @@ import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assign
import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import {
mockParent,
workItemDatesSubscriptionResponse,
@@ -40,11 +39,11 @@ import {
workItemAssigneesSubscriptionResponse,
workItemMilestoneSubscriptionResponse,
projectWorkItemResponse,
+ objectiveType,
} from '../mock_data';
describe('WorkItemDetail component', () => {
let wrapper;
- useLocalStorageSpy();
Vue.use(VueApollo);
@@ -81,8 +80,7 @@ describe('WorkItemDetail component', () => {
const findParentButton = () => findParent().findComponent(GlButton);
const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]');
const findWorkItemType = () => wrapper.find('[data-testid="work-item-type"]');
- const findWorkItemInformationAlert = () => wrapper.findComponent(WorkItemInformation);
- const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+ const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
const createComponent = ({
isModal = false,
@@ -92,9 +90,9 @@ describe('WorkItemDetail component', () => {
subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
error = undefined,
+ workItemsMvcEnabled = false,
workItemsMvc2Enabled = false,
fetchByIid = false,
- iidPathQueryParam = undefined,
} = {}) => {
const handlers = [
[workItemQuery, handler],
@@ -108,7 +106,7 @@ describe('WorkItemDetail component', () => {
wrapper = shallowMount(WorkItemDetail, {
apolloProvider: createMockApollo(handlers),
- propsData: { isModal, workItemId, iid: '1' },
+ propsData: { isModal, workItemId, workItemIid: '1' },
data() {
return {
updateInProgress,
@@ -117,11 +115,14 @@ describe('WorkItemDetail component', () => {
},
provide: {
glFeatures: {
+ workItemsMvc: workItemsMvcEnabled,
workItemsMvc2: workItemsMvc2Enabled,
useIidInWorkItemsPath: fetchByIid,
},
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
+ hasOkrsFeature: true,
+ hasIssuableHealthStatusFeature: true,
projectNamespace: 'namespace',
fullPath: 'group/project',
},
@@ -129,18 +130,12 @@ describe('WorkItemDetail component', () => {
WorkItemWeight: true,
WorkItemIteration: true,
},
- mocks: {
- $route: {
- query: {
- iid_path: iidPathQueryParam,
- },
- },
- },
});
};
afterEach(() => {
wrapper.destroy();
+ setWindowLocation('');
});
describe('when there is no `workItemId` prop', () => {
@@ -406,9 +401,31 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemType().exists()).toBe(false);
});
- it('sets the parent breadcrumb URL', () => {
+ it('shows parent breadcrumb icon', () => {
+ expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName);
+ });
+
+ it('sets the parent breadcrumb URL pointing to issue page when parent type is `Issue`', () => {
expect(findParentButton().attributes().href).toBe('../../issues/5');
});
+
+ it('sets the parent breadcrumb URL based on parent webUrl when parent type is not `Issue`', async () => {
+ const mockParentObjective = {
+ parent: {
+ ...mockParent.parent,
+ workItemType: {
+ id: mockParent.parent.workItemType.id,
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+ },
+ },
+ };
+ const parentResponse = workItemResponseFactory(mockParentObjective);
+ createComponent({ handler: jest.fn().mockResolvedValue(parentResponse) });
+ await waitForPromises();
+
+ expect(findParentButton().attributes().href).toBe(mockParentObjective.parent.webUrl);
+ });
});
});
@@ -563,7 +580,7 @@ describe('WorkItemDetail component', () => {
`('$description', async ({ milestoneWidgetPresent, exists }) => {
const response = workItemResponseFactory({ milestoneWidgetPresent });
const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler, workItemsMvc2Enabled: true });
+ createComponent({ handler });
await waitForPromises();
expect(findWorkItemMilestone().exists()).toBe(exists);
@@ -594,24 +611,6 @@ describe('WorkItemDetail component', () => {
});
});
- describe('work item information', () => {
- beforeEach(() => {
- createComponent();
- return waitForPromises();
- });
-
- it('is visible when viewed for the first time and sets localStorage value', async () => {
- localStorage.clear();
- expect(findWorkItemInformationAlert().exists()).toBe(true);
- expect(findLocalStorageSync().props('value')).toBe(true);
- });
-
- it('is not visible after reading local storage input', async () => {
- await findLocalStorageSync().vm.$emit('input', false);
- expect(findWorkItemInformationAlert().exists()).toBe(false);
- });
- });
-
it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is false', async () => {
createComponent();
await waitForPromises();
@@ -633,6 +632,8 @@ describe('WorkItemDetail component', () => {
});
it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present', async () => {
+ setWindowLocation(`?iid_path=true`);
+
createComponent({ fetchByIid: true, iidPathQueryParam: 'true' });
await waitForPromises();
@@ -642,4 +643,24 @@ describe('WorkItemDetail component', () => {
iid: '1',
});
});
+
+ describe('hierarchy widget', () => {
+ it('does not render children tree by default', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findHierarchyTree().exists()).toBe(false);
+ });
+
+ it('renders children tree when work item is an Objective', async () => {
+ const objectiveWorkItem = workItemResponseFactory({
+ workItemType: objectiveType,
+ });
+ const handler = jest.fn().mockResolvedValue(objectiveWorkItem);
+ createComponent({ handler });
+ await waitForPromises();
+
+ expect(findHierarchyTree().exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_information_spec.js b/spec/frontend/work_items/components/work_item_information_spec.js
deleted file mode 100644
index 887c5f615e9..00000000000
--- a/spec/frontend/work_items/components/work_item_information_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { GlAlert, GlLink } from '@gitlab/ui';
-import WorkItemInformation from '~/work_items/components/work_item_information.vue';
-import { helpPagePath } from '~/helpers/help_page_helper';
-
-const createComponent = () => mount(WorkItemInformation);
-
-describe('Work item information alert', () => {
- let wrapper;
- const tasksHelpPath = helpPagePath('user/tasks');
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findHelpLink = () => wrapper.findComponent(GlLink);
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should be visible', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('should emit `work-item-banner-dismissed` event when cross icon is clicked', () => {
- findAlert().vm.$emit('dismiss');
- expect(wrapper.emitted('work-item-banner-dismissed').length).toBe(1);
- });
-
- it('the alert variant should be tip', () => {
- expect(findAlert().props('variant')).toBe('tip');
- });
-
- it('should have the correct text for title', () => {
- expect(findAlert().props('title')).toBe(WorkItemInformation.i18n.tasksInformationTitle);
- });
-
- it('should have the correct link to work item link', () => {
- expect(findHelpLink().exists()).toBe(true);
- expect(findHelpLink().attributes('href')).toBe(tasksHelpPath);
- });
-});
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 9f7659b3f8d..083bb5bc4a4 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,7 @@ 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 '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
+import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
@@ -49,6 +49,7 @@ describe('WorkItemLabels component', () => {
searchQueryHandler = successSearchQueryHandler,
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
fetchByIid = false,
+ queryVariables = { id: workItemId },
} = {}) => {
const apolloProvider = createMockApollo([
[workItemQuery, workItemQueryHandler],
@@ -63,9 +64,7 @@ describe('WorkItemLabels component', () => {
workItemId,
canUpdate,
fullPath: 'test-project-path',
- queryVariables: {
- id: workItemId,
- },
+ queryVariables,
fetchByIid,
},
attachTo: document.body,
@@ -251,4 +250,11 @@ describe('WorkItemLabels component', () => {
expect(workItemQuerySuccess).not.toHaveBeenCalled();
expect(workItemByIidResponseHandler).toHaveBeenCalled();
});
+
+ it('skips calling the handlers when missing the needed queryVariables', async () => {
+ createComponent({ queryVariables: {}, fetchByIid: false });
+ await waitForPromises();
+
+ expect(workItemQuerySuccess).not.toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
new file mode 100644
index 00000000000..5fbd8e7e1a7
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
@@ -0,0 +1,35 @@
+import { GlDropdownSectionHeader } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+
+import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+
+const createComponent = () => {
+ return extendedWrapper(shallowMount(OkrActionsSplitButton));
+};
+
+describe('RelatedItemsTree', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('OkrActionsSplitButton', () => {
+ describe('template', () => {
+ it('renders objective and key results sections', () => {
+ expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(0).text()).toContain(
+ 'Objective',
+ );
+
+ expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(1).text()).toContain(
+ 'Key result',
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
new file mode 100644
index 00000000000..47489d4796b
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
@@ -0,0 +1,67 @@
+import { GlLabel, GlAvatarsInline } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import ItemMilestone from '~/issuable/components/issue_milestone.vue';
+import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/work_item_link_child_metadata.vue';
+
+import { mockMilestone, mockAssignees, mockLabels } from '../../mock_data';
+
+describe('WorkItemLinkChildMetadata', () => {
+ let wrapper;
+
+ const createComponent = ({
+ allowsScopedLabels = true,
+ milestone = mockMilestone,
+ assignees = mockAssignees,
+ labels = mockLabels,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemLinkChildMetadata, {
+ propsData: {
+ allowsScopedLabels,
+ milestone,
+ assignees,
+ labels,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders milestone link button', () => {
+ const milestoneLink = wrapper.findComponent(ItemMilestone);
+
+ expect(milestoneLink.exists()).toBe(true);
+ expect(milestoneLink.props('milestone')).toEqual(mockMilestone);
+ });
+
+ it('renders avatars for assignees', () => {
+ const avatars = wrapper.findComponent(GlAvatarsInline);
+
+ expect(avatars.exists()).toBe(true);
+ expect(avatars.props()).toMatchObject({
+ avatars: mockAssignees,
+ collapsed: true,
+ maxVisible: 2,
+ avatarSize: 24,
+ badgeTooltipProp: 'name',
+ badgeSrOnlyText: '',
+ });
+ });
+
+ it('renders labels', () => {
+ const labels = wrapper.findAllComponents(GlLabel);
+ const mockLabel = mockLabels[0];
+
+ expect(labels).toHaveLength(mockLabels.length);
+ expect(labels.at(0).props()).toMatchObject({
+ title: mockLabel.title,
+ backgroundColor: mockLabel.color,
+ description: mockLabel.description,
+ scoped: false,
+ });
+ expect(labels.at(1).props('scoped')).toBe(true); // Second label is scoped
+ });
+});
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 1d5472a0473..73d498ad055 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
@@ -1,33 +1,73 @@
-import { GlButton, GlIcon } from '@gitlab/ui';
+import { GlIcon } from '@gitlab/ui';
+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 waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
+import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
+import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/work_item_link_child_metadata.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
+import WorkItemTreeChildren from '~/work_items/components/work_item_links/work_item_tree_children.vue';
+import {
+ WIDGET_TYPE_HIERARCHY,
+ TASK_TYPE_NAME,
+ WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+} from '~/work_items/constants';
-import { workItemTask, confidentialWorkItemTask, closedWorkItemTask } from '../../mock_data';
+import {
+ workItemTask,
+ workItemObjectiveWithChild,
+ workItemObjectiveNoMetadata,
+ confidentialWorkItemTask,
+ closedWorkItemTask,
+ mockMilestone,
+ mockAssignees,
+ mockLabels,
+ workItemHierarchyTreeResponse,
+ workItemHierarchyTreeFailureResponse,
+} from '../../mock_data';
+
+jest.mock('~/flash');
describe('WorkItemLinkChild', () => {
const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
let wrapper;
+ let getWorkItemTreeQueryHandler;
+
+ Vue.use(VueApollo);
const createComponent = ({
projectPath = 'gitlab-org/gitlab-test',
canUpdate = true,
issuableGid = WORK_ITEM_ID,
childItem = workItemTask,
+ workItemType = TASK_TYPE_NAME,
+ apolloProvider = null,
} = {}) => {
+ getWorkItemTreeQueryHandler = jest.fn().mockResolvedValue(workItemHierarchyTreeResponse);
+
wrapper = shallowMountExtended(WorkItemLinkChild, {
+ apolloProvider:
+ apolloProvider || createMockApollo([[getWorkItemTreeQuery, getWorkItemTreeQueryHandler]]),
propsData: {
projectPath,
canUpdate,
issuableGid,
childItem,
+ workItemType,
},
});
};
+ beforeEach(() => {
+ createAlert.mockClear();
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -66,7 +106,7 @@ describe('WorkItemLinkChild', () => {
beforeEach(() => {
createComponent();
- titleEl = wrapper.findComponent(GlButton);
+ titleEl = wrapper.findByTestId('item-title');
});
it('renders item title', () => {
@@ -76,16 +116,52 @@ describe('WorkItemLinkChild', () => {
it.each`
action | event | emittedEvent
- ${'clicking'} | ${'click'} | ${'click'}
${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'}
${'doing mouseout on'} | ${'mouseout'} | ${'mouseout'}
`('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => {
+ titleEl.vm.$emit(event);
+
+ expect(wrapper.emitted(emittedEvent)).toEqual([[]]);
+ });
+
+ it('emits click event with correct parameters on clicking title', () => {
const eventObj = {
preventDefault: jest.fn(),
};
- titleEl.vm.$emit(event, eventObj);
+ titleEl.vm.$emit('click', eventObj);
- expect(wrapper.emitted(emittedEvent)).toEqual([[workItemTask.id, eventObj]]);
+ expect(wrapper.emitted('click')).toEqual([[eventObj]]);
+ });
+ });
+
+ describe('item metadata', () => {
+ const findMetadataComponent = () => wrapper.findComponent(WorkItemLinkChildMetadata);
+
+ beforeEach(() => {
+ createComponent({
+ childItem: workItemObjectiveWithChild,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ });
+ });
+
+ it('renders item metadata component when item has metadata present', () => {
+ const metadataEl = findMetadataComponent();
+ expect(metadataEl.exists()).toBe(true);
+ expect(metadataEl.props()).toMatchObject({
+ allowsScopedLabels: true,
+ milestone: mockMilestone,
+ assignees: mockAssignees,
+ labels: mockLabels,
+ });
+ });
+
+ it('does not render item metadata component when item has no metadata present', () => {
+ createComponent({
+ childItem: workItemObjectiveNoMetadata,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ });
+
+ expect(findMetadataComponent().exists()).toBe(false);
});
});
@@ -116,7 +192,78 @@ describe('WorkItemLinkChild', () => {
it('removeChild event on menu triggers `click-remove-child` event', () => {
itemMenuEl.vm.$emit('removeChild');
- expect(wrapper.emitted('remove')).toEqual([[workItemTask.id]]);
+ expect(wrapper.emitted('removeChild')).toEqual([[workItemTask.id]]);
+ });
+ });
+
+ describe('nested children', () => {
+ const findExpandButton = () => wrapper.findByTestId('expand-child');
+ const findTreeChildren = () => wrapper.findComponent(WorkItemTreeChildren);
+
+ beforeEach(() => {
+ getWorkItemTreeQueryHandler.mockClear();
+ createComponent({
+ childItem: workItemObjectiveWithChild,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ });
+ });
+
+ it('displays expand button when item has children, children are not displayed by default', () => {
+ expect(findExpandButton().exists()).toBe(true);
+ expect(findTreeChildren().exists()).toBe(false);
+ });
+
+ it('fetches and displays children of item when clicking on expand button', async () => {
+ await findExpandButton().vm.$emit('click');
+
+ expect(findExpandButton().props('loading')).toBe(true);
+ await waitForPromises();
+
+ expect(getWorkItemTreeQueryHandler).toHaveBeenCalled();
+ expect(findTreeChildren().exists()).toBe(true);
+
+ const widgetHierarchy = workItemHierarchyTreeResponse.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
+ );
+ expect(findTreeChildren().props('children')).toEqual(widgetHierarchy.children.nodes);
+ });
+
+ it('does not fetch children if already fetched once while clicking expand button', async () => {
+ findExpandButton().vm.$emit('click'); // Expand for the first time
+ await waitForPromises();
+
+ expect(findTreeChildren().exists()).toBe(true);
+
+ await findExpandButton().vm.$emit('click'); // Collapse
+ findExpandButton().vm.$emit('click'); // Expand again
+ await waitForPromises();
+
+ expect(getWorkItemTreeQueryHandler).toHaveBeenCalledTimes(1); // ensure children were fetched only once.
+ expect(findTreeChildren().exists()).toBe(true);
+ });
+
+ it('calls createAlert when children fetch request fails on clicking expand button', async () => {
+ const getWorkItemTreeQueryFailureHandler = jest
+ .fn()
+ .mockRejectedValue(workItemHierarchyTreeFailureResponse);
+ const apolloProvider = createMockApollo([
+ [getWorkItemTreeQuery, getWorkItemTreeQueryFailureHandler],
+ ]);
+
+ createComponent({
+ childItem: workItemObjectiveWithChild,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ apolloProvider,
+ });
+
+ findExpandButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.any(Object),
+ message: 'Something went wrong while fetching children.',
+ });
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
index 071d5fb715a..bbe460a55ba 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
@@ -33,7 +33,7 @@ describe('WorkItemLinksForm', () => {
typesResponse = projectWorkItemTypesQueryResponse,
parentConfidential = false,
hasIterationsFeature = false,
- workItemsMvc2Enabled = false,
+ workItemsMvcEnabled = false,
parentIteration = null,
formType = FORM_TYPES.create,
} = {}) => {
@@ -52,7 +52,7 @@ describe('WorkItemLinksForm', () => {
},
provide: {
glFeatures: {
- workItemsMvc2: workItemsMvc2Enabled,
+ workItemsMvc: workItemsMvcEnabled,
},
projectPath: 'project/path',
hasIterationsFeature,
@@ -165,23 +165,8 @@ describe('WorkItemLinksForm', () => {
});
describe('associate iteration with task', () => {
- it('does not update iteration when mvc2 feature flag is not enabled', async () => {
- await createComponent({
- hasIterationsFeature: true,
- parentIteration: mockParentIteration,
- });
-
- findInput().vm.$emit('input', 'Create task test');
-
- findForm().vm.$emit('submit', {
- preventDefault: jest.fn(),
- });
- await waitForPromises();
- expect(updateMutationResolver).not.toHaveBeenCalled();
- });
it('updates when parent has an iteration associated', async () => {
await createComponent({
- workItemsMvc2Enabled: true,
hasIterationsFeature: true,
parentIteration: mockParentIteration,
});
@@ -191,18 +176,23 @@ describe('WorkItemLinksForm', () => {
preventDefault: jest.fn(),
});
await waitForPromises();
- expect(updateMutationResolver).toHaveBeenCalledWith({
+ expect(createMutationResolver).toHaveBeenCalledWith({
input: {
- id: 'gid://gitlab/WorkItem/1',
+ title: 'Create task test',
+ projectPath: 'project/path',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ confidential: false,
iterationWidget: {
iterationId: mockParentIteration.id,
},
},
});
});
- it('does not update when parent has no iteration associated', async () => {
+ it('does not send the iteration widget to mutation when parent has no iteration associated', async () => {
await createComponent({
- workItemsMvc2Enabled: true,
hasIterationsFeature: true,
});
findInput().vm.$emit('input', 'Create task test');
@@ -211,7 +201,20 @@ describe('WorkItemLinksForm', () => {
preventDefault: jest.fn(),
});
await waitForPromises();
- expect(updateMutationResolver).not.toHaveBeenCalled();
+ expect(createMutationResolver).not.toHaveBeenCalledWith({
+ input: {
+ title: 'Create task test',
+ projectPath: 'project/path',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ confidential: false,
+ iterationWidget: {
+ iterationId: mockParentIteration.id,
+ },
+ },
+ });
});
});
});
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 66ce2c1becf..a61de78c623 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
@@ -4,20 +4,25 @@ 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 setWindowLocation from 'helpers/set_window_location_helper';
+import { stubComponent } from 'helpers/stub_component';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
+import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import { FORM_TYPES } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
workItemHierarchyResponse,
workItemHierarchyEmptyResponse,
workItemHierarchyNoUpdatePermissionResponse,
changeWorkItemParentMutationResponse,
workItemQueryResponse,
+ projectWorkItemResponse,
} from '../../mock_data';
Vue.use(VueApollo);
@@ -55,6 +60,7 @@ const issueDetailsResponse = (confidential = false) => ({
},
},
});
+const showModal = jest.fn();
describe('WorkItemLinks', () => {
let wrapper;
@@ -71,6 +77,7 @@ describe('WorkItemLinks', () => {
.mockResolvedValue(changeWorkItemParentMutationResponse);
const childWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ const childWorkItemByIidHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
const createComponent = async ({
data = {},
@@ -78,6 +85,7 @@ describe('WorkItemLinks', () => {
mutationHandler = mutationChangeParentHandler,
issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse()),
hasIterationsFeature = false,
+ fetchByIid = false,
} = {}) => {
mockApollo = createMockApollo(
[
@@ -85,6 +93,7 @@ describe('WorkItemLinks', () => {
[changeWorkItemParentMutation, mutationHandler],
[workItemQuery, childWorkItemQueryHandler],
[issueDetailsQuery, issueDetailsQueryHandler],
+ [workItemByIidQuery, childWorkItemByIidHandler],
],
{},
{ addTypename: true },
@@ -100,12 +109,22 @@ describe('WorkItemLinks', () => {
projectPath: 'project/path',
iid: '1',
hasIterationsFeature,
+ glFeatures: {
+ useIidInWorkItemsPath: fetchByIid,
+ },
},
propsData: { issuableId: 1 },
apolloProvider: mockApollo,
mocks: {
$toast,
},
+ stubs: {
+ WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
+ methods: {
+ show: showModal,
+ },
+ }),
+ },
});
await waitForPromises();
@@ -130,6 +149,7 @@ describe('WorkItemLinks', () => {
afterEach(() => {
wrapper.destroy();
mockApollo = null;
+ setWindowLocation('');
});
it('is expanded by default', () => {
@@ -237,7 +257,7 @@ describe('WorkItemLinks', () => {
});
it('calls correct mutation with correct variables', async () => {
- firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
+ firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id);
await waitForPromises();
@@ -252,7 +272,7 @@ describe('WorkItemLinks', () => {
});
it('shows toast when mutation succeeds', async () => {
- firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
+ firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id);
await waitForPromises();
@@ -264,56 +284,164 @@ describe('WorkItemLinks', () => {
it('renders correct number of children after removal', async () => {
expect(findWorkItemLinkChildItems()).toHaveLength(4);
- firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
+ firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id);
await waitForPromises();
expect(findWorkItemLinkChildItems()).toHaveLength(3);
});
});
- describe('prefetching child items', () => {
- let firstChild;
-
- beforeEach(async () => {
- await createComponent();
+ describe('when parent item is confidential', () => {
+ it('passes correct confidentiality status to form', async () => {
+ await createComponent({
+ issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)),
+ });
+ findToggleFormDropdown().vm.$emit('click');
+ findToggleAddFormButton().vm.$emit('click');
+ await nextTick();
- firstChild = findFirstWorkItemLinkChild();
+ expect(findAddLinksForm().props('parentConfidential')).toBe(true);
});
+ });
- it('does not fetch the child work item before hovering work item links', () => {
- expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ describe('when work item is fetched by id', () => {
+ describe('prefetching child items', () => {
+ let firstChild;
+
+ beforeEach(async () => {
+ await createComponent();
+
+ firstChild = findFirstWorkItemLinkChild();
+ });
+
+ it('does not fetch the child work item by id before hovering work item links', () => {
+ expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ });
+
+ it('fetches the child work item by id if link is hovered for 250+ ms', async () => {
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ await waitForPromises();
+
+ expect(childWorkItemQueryHandler).toHaveBeenCalledWith({
+ id: 'gid://gitlab/WorkItem/2',
+ });
+ });
+
+ it('does not fetch the child work item by id if link is hovered for less than 250 ms', async () => {
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
+ jest.advanceTimersByTime(200);
+ firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id);
+ await waitForPromises();
+
+ expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ });
+
+ it('does not fetch work item by iid if link is hovered for 250+ ms', async () => {
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ await waitForPromises();
+
+ expect(childWorkItemByIidHandler).not.toHaveBeenCalled();
+ });
});
- it('fetches the child work item if link is hovered for 250+ ms', async () => {
- firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
- jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- await waitForPromises();
+ it('starts prefetching work item by id if URL contains work item id', async () => {
+ setWindowLocation('?work_item_id=5');
+ await createComponent();
expect(childWorkItemQueryHandler).toHaveBeenCalledWith({
- id: 'gid://gitlab/WorkItem/2',
+ id: 'gid://gitlab/WorkItem/5',
});
});
- it('does not fetch the child work item if link is hovered for less than 250 ms', async () => {
- firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
- jest.advanceTimersByTime(200);
- firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id);
- await waitForPromises();
+ it('does not open the modal if work item id URL parameter is not found in child items', async () => {
+ setWindowLocation('?work_item_id=555');
+ await createComponent();
+
+ expect(showModal).not.toHaveBeenCalled();
+ expect(wrapper.findComponent(WorkItemDetailModal).props('workItemId')).toBe(null);
+ });
+
+ it('opens the modal if work item id URL parameter is found in child items', async () => {
+ setWindowLocation('?work_item_id=2');
+ await createComponent();
- expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ expect(showModal).toHaveBeenCalled();
+ expect(wrapper.findComponent(WorkItemDetailModal).props('workItemId')).toBe(
+ 'gid://gitlab/WorkItem/2',
+ );
});
});
- describe('when parent item is confidential', () => {
- it('passes correct confidentiality status to form', async () => {
- await createComponent({
- issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)),
+ describe('when work item is fetched by iid', () => {
+ describe('prefetching child items', () => {
+ let firstChild;
+
+ beforeEach(async () => {
+ setWindowLocation('?iid_path=true');
+ await createComponent({ fetchByIid: true });
+
+ firstChild = findFirstWorkItemLinkChild();
});
- findToggleFormDropdown().vm.$emit('click');
- findToggleAddFormButton().vm.$emit('click');
- await nextTick();
- expect(findAddLinksForm().props('parentConfidential')).toBe(true);
+ it('does not fetch the child work item by iid before hovering work item links', () => {
+ expect(childWorkItemByIidHandler).not.toHaveBeenCalled();
+ });
+
+ it('fetches the child work item by iid if link is hovered for 250+ ms', async () => {
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ await waitForPromises();
+
+ expect(childWorkItemByIidHandler).toHaveBeenCalledWith({
+ fullPath: 'project/path',
+ iid: '2',
+ });
+ });
+
+ it('does not fetch the child work item by iid if link is hovered for less than 250 ms', async () => {
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
+ jest.advanceTimersByTime(200);
+ firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id);
+ await waitForPromises();
+
+ expect(childWorkItemByIidHandler).not.toHaveBeenCalled();
+ });
+
+ it('does not fetch work item by id if link is hovered for 250+ ms', async () => {
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ await waitForPromises();
+
+ expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ });
});
+
+ it('starts prefetching work item by iid if URL contains work item id', async () => {
+ setWindowLocation('?work_item_iid=5&iid_path=true');
+ await createComponent({ fetchByIid: true });
+
+ expect(childWorkItemByIidHandler).toHaveBeenCalledWith({
+ iid: '5',
+ fullPath: 'project/path',
+ });
+ });
+ });
+
+ it('does not open the modal if work item iid URL parameter is not found in child items', async () => {
+ setWindowLocation('?work_item_iid=555&iid_path=true');
+ await createComponent({ fetchByIid: true });
+
+ expect(showModal).not.toHaveBeenCalled();
+ expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe(null);
+ });
+
+ it('opens the modal if work item iid URL parameter is found in child items', async () => {
+ setWindowLocation('?work_item_iid=2&iid_path=true');
+ await createComponent({ fetchByIid: true });
+
+ expect(showModal).toHaveBeenCalled();
+ expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe('2');
});
});
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
new file mode 100644
index 00000000000..96211e12755
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
@@ -0,0 +1,147 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
+import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
+import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
+import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+import {
+ FORM_TYPES,
+ WORK_ITEM_TYPE_ENUM_OBJECTIVE,
+ WORK_ITEM_TYPE_ENUM_KEY_RESULT,
+} from '~/work_items/constants';
+import { childrenWorkItems, workItemObjectiveWithChild } from '../../mock_data';
+
+describe('WorkItemTree', () => {
+ let getWorkItemQueryHandler;
+ let wrapper;
+
+ const findToggleButton = () => wrapper.findByTestId('toggle-tree');
+ const findTreeBody = () => wrapper.findByTestId('tree-body');
+ const findEmptyState = () => wrapper.findByTestId('tree-empty');
+ const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton);
+ const findForm = () => wrapper.findComponent(WorkItemLinksForm);
+ const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
+
+ Vue.use(VueApollo);
+
+ const createComponent = ({
+ workItemType = 'Objective',
+ children = childrenWorkItems,
+ apolloProvider = null,
+ } = {}) => {
+ const mockWorkItemResponse = {
+ data: {
+ workItem: {
+ ...workItemObjectiveWithChild,
+ workItemType: {
+ ...workItemObjectiveWithChild.workItemType,
+ name: workItemType,
+ },
+ },
+ },
+ };
+ getWorkItemQueryHandler = jest.fn().mockResolvedValue(mockWorkItemResponse);
+
+ wrapper = shallowMountExtended(WorkItemTree, {
+ apolloProvider:
+ apolloProvider || createMockApollo([[workItemQuery, getWorkItemQueryHandler]]),
+ propsData: {
+ workItemType,
+ workItemId: 'gid://gitlab/WorkItem/515',
+ children,
+ projectPath: 'test/project',
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('is expanded by default and displays Add button', () => {
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
+ expect(findTreeBody().exists()).toBe(true);
+ expect(findToggleFormSplitButton().exists()).toBe(true);
+ });
+
+ it('collapses on click toggle button', async () => {
+ findToggleButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
+ expect(findTreeBody().exists()).toBe(false);
+ });
+
+ it('displays empty state if there are no children', () => {
+ createComponent({ children: [] });
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('renders all hierarchy widget children', () => {
+ expect(findWorkItemLinkChildItems()).toHaveLength(4);
+ });
+
+ it('does not display form by default', () => {
+ expect(findForm().exists()).toBe(false);
+ });
+
+ it.each`
+ option | event | formType | childType
+ ${'New objective'} | ${'showCreateObjectiveForm'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE}
+ ${'Existing objective'} | ${'showAddObjectiveForm'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE}
+ ${'New key result'} | ${'showCreateKeyResultForm'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT}
+ ${'Existing key result'} | ${'showAddKeyResultForm'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT}
+ `(
+ 'when selecting $option from split button, renders the form passing $formType and $childType',
+ async ({ event, formType, childType }) => {
+ findToggleFormSplitButton().vm.$emit(event);
+ await nextTick();
+
+ expect(findForm().exists()).toBe(true);
+ expect(findForm().props('formType')).toBe(formType);
+ expect(findForm().props('childrenType')).toBe(childType);
+ },
+ );
+
+ it('remove event on child triggers `removeChild` event', () => {
+ const firstChild = findWorkItemLinkChildItems().at(0);
+ firstChild.vm.$emit('removeChild', 'gid://gitlab/WorkItem/2');
+
+ expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]);
+ });
+
+ it.each`
+ description | workItemType | prefetch
+ ${'prefetches'} | ${'Issue'} | ${true}
+ ${'does not prefetch'} | ${'Objective'} | ${false}
+ `(
+ '$description work-item-link-child on mouseover when workItemType is "$workItemType"',
+ async ({ workItemType, prefetch }) => {
+ createComponent({ workItemType });
+ const firstChild = findWorkItemLinkChildItems().at(0);
+ firstChild.vm.$emit('mouseover', childrenWorkItems[0]);
+ await nextTick();
+ await waitForPromises();
+
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+
+ if (prefetch) {
+ expect(getWorkItemQueryHandler).toHaveBeenCalled();
+ } else {
+ expect(getWorkItemQueryHandler).not.toHaveBeenCalled();
+ }
+ },
+ );
+});
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 60ba2b55f76..5997de01274 100644
--- a/spec/frontend/work_items/components/work_item_milestone_spec.js
+++ b/spec/frontend/work_items/components/work_item_milestone_spec.js
@@ -179,6 +179,18 @@ describe('WorkItemMilestone component', () => {
createComponent({ canUpdate: true });
});
+ it('calls successSearchQueryHandler with variables when dropdown is opened', async () => {
+ showDropdown();
+ await nextTick();
+
+ expect(successSearchQueryHandler).toHaveBeenCalledWith({
+ first: 20,
+ fullPath: 'full-path',
+ state: 'active',
+ title: '',
+ });
+ });
+
it('shows the skeleton loader when the items are being fetched on click', async () => {
showDropdown();
await nextTick();
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
new file mode 100644
index 00000000000..ed68d214fc9
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -0,0 +1,107 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import SystemNote from '~/work_items/components/notes/system_note.vue';
+import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
+import workItemNotesQuery from '~/work_items/graphql/work_item_notes.query.graphql';
+import workItemNotesByIidQuery from '~/work_items/graphql/work_item_notes_by_iid.query.graphql';
+import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
+import {
+ mockWorkItemNotesResponse,
+ workItemQueryResponse,
+ mockWorkItemNotesByIidResponse,
+} from '../mock_data';
+
+const mockWorkItemId = workItemQueryResponse.data.workItem.id;
+const mockNotesWidgetResponse = mockWorkItemNotesResponse.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_NOTES,
+);
+
+const mockNotesByIidWidgetResponse = mockWorkItemNotesByIidResponse.data.workspace.workItems.nodes[0].widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_NOTES,
+);
+
+describe('WorkItemNotes component', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote);
+ const findActivityLabel = () => wrapper.find('label');
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse);
+ const workItemNotesByIidQueryHandler = jest
+ .fn()
+ .mockResolvedValue(mockWorkItemNotesByIidResponse);
+
+ const createComponent = ({ workItemId = mockWorkItemId, fetchByIid = false } = {}) => {
+ wrapper = shallowMount(WorkItemNotes, {
+ apolloProvider: createMockApollo([
+ [workItemNotesQuery, workItemNotesQueryHandler],
+ [workItemNotesByIidQuery, workItemNotesByIidQueryHandler],
+ ]),
+ propsData: {
+ workItemId,
+ queryVariables: {
+ id: workItemId,
+ },
+ fullPath: 'test-path',
+ fetchByIid,
+ },
+ provide: {
+ glFeatures: {
+ useIidInWorkItemsPath: fetchByIid,
+ },
+ },
+ });
+ };
+
+ beforeEach(async () => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders activity label', () => {
+ expect(findActivityLabel().exists()).toBe(true);
+ });
+
+ describe('when notes are loading', () => {
+ it('renders skeleton loader', () => {
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('does not render system notes', () => {
+ expect(findAllSystemNotes().exists()).toBe(false);
+ });
+ });
+
+ describe('when notes have been loaded', () => {
+ it('does not render skeleton loader', () => {
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('renders system notes to the length of the response', async () => {
+ await waitForPromises();
+ expect(findAllSystemNotes()).toHaveLength(mockNotesWidgetResponse.discussions.nodes.length);
+ });
+ });
+
+ describe('when the notes are fetched by `iid`', () => {
+ beforeEach(async () => {
+ createComponent({ workItemId: mockWorkItemId, fetchByIid: true });
+ await waitForPromises();
+ });
+
+ it('shows the notes list', () => {
+ expect(findAllSystemNotes()).toHaveLength(
+ mockNotesByIidWidgetResponse.discussions.nodes.length,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 635a1f326f8..850672b68d0 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -36,6 +36,16 @@ export const mockLabels = [
},
];
+export const mockMilestone = {
+ __typename: 'Milestone',
+ id: 'gid://gitlab/Milestone/30',
+ title: 'v4.0',
+ state: 'active',
+ expired: false,
+ startDate: '2022-10-17',
+ dueDate: '2022-10-24',
+};
+
export const workItemQueryResponse = {
data: {
workItem: {
@@ -85,11 +95,18 @@ export const workItemQueryResponse = {
{
__typename: 'WorkItemWidgetHierarchy',
type: 'HIERARCHY',
+ hasChildren: true,
parent: {
id: 'gid://gitlab/Issue/1',
iid: '5',
title: 'Parent title',
confidential: false,
+ webUrl: 'http://gdk.test/gitlab-org/gitlab/-/issues/1',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/1',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ },
},
children: {
nodes: [
@@ -97,6 +114,20 @@ export const workItemQueryResponse = {
id: 'gid://gitlab/WorkItem/444',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ confidential: false,
+ title: '123',
+ state: 'OPEN',
+ workItemType: {
+ id: '1',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ },
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ hasChildren: false,
+ },
+ ],
},
],
},
@@ -138,13 +169,25 @@ export const updateWorkItemMutationResponse = {
},
widgets: [
{
+ type: 'HIERARCHY',
children: {
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ confidential: false,
+ title: '123',
+ state: 'OPEN',
+ workItemType: {
+ id: '1',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ },
},
],
},
+ __typename: 'WorkItemConnection',
},
{
__typename: 'WorkItemWidgetAssignees',
@@ -177,6 +220,12 @@ export const mockParent = {
iid: '5',
title: 'Parent title',
confidential: false,
+ webUrl: 'http://gdk.test/gitlab-org/gitlab/-/issues/1',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/1',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ },
},
};
@@ -193,6 +242,20 @@ export const descriptionHtmlWithCheckboxes = `
</ul>
`;
+const taskType = {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
+};
+
+export const objectiveType = {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/2411',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+};
+
export const workItemResponseFactory = ({
canUpdate = false,
canDelete = false,
@@ -201,8 +264,10 @@ export const workItemResponseFactory = ({
datesWidgetPresent = true,
labelsWidgetPresent = true,
weightWidgetPresent = true,
+ progressWidgetPresent = true,
milestoneWidgetPresent = true,
iterationWidgetPresent = true,
+ healthStatusWidgetPresent = true,
confidential = false,
canInviteMembers = false,
allowsScopedLabels = false,
@@ -210,6 +275,7 @@ export const workItemResponseFactory = ({
lastEditedBy = null,
withCheckboxes = false,
parent = mockParent.parent,
+ workItemType = taskType,
} = {}) => ({
data: {
workItem: {
@@ -227,12 +293,7 @@ export const workItemResponseFactory = ({
id: '1',
fullPath: 'test-project-path',
},
- workItemType: {
- __typename: 'WorkItemType',
- id: 'gid://gitlab/WorkItems::Type/5',
- name: 'Task',
- iconName: 'issue-type-task',
- },
+ workItemType,
userPermissions: {
deleteWorkItem: canDelete,
updateWorkItem: canUpdate,
@@ -298,26 +359,51 @@ export const workItemResponseFactory = ({
},
}
: { type: 'MOCK TYPE' },
+ progressWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetProgress',
+ type: 'PROGRESS',
+ progress: 0,
+ }
+ : { type: 'MOCK TYPE' },
milestoneWidgetPresent
? {
__typename: 'WorkItemWidgetMilestone',
type: 'MILESTONE',
- milestone: {
- expired: false,
- id: 'gid://gitlab/Milestone/30',
- title: 'v4.0',
- },
+ milestone: mockMilestone,
+ }
+ : { type: 'MOCK TYPE' },
+ healthStatusWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetHealthStatus',
+ type: 'HEALTH_STATUS',
+ healthStatus: 'onTrack',
}
: { type: 'MOCK TYPE' },
{
__typename: 'WorkItemWidgetHierarchy',
type: 'HIERARCHY',
+ hasChildren: true,
children: {
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ confidential: false,
+ title: '123',
+ state: 'OPEN',
+ workItemType: {
+ id: '1',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ },
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ hasChildren: false,
+ },
+ ],
},
],
},
@@ -637,6 +723,8 @@ export const workItemHierarchyEmptyResponse = {
id: 'gid://gitlab/WorkItem/1',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/6',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
__typename: 'WorkItemType',
},
title: 'New title',
@@ -660,6 +748,7 @@ export const workItemHierarchyEmptyResponse = {
{
type: 'HIERARCHY',
parent: null,
+ hasChildren: false,
children: {
nodes: [],
__typename: 'WorkItemConnection',
@@ -678,6 +767,8 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
id: 'gid://gitlab/WorkItem/1',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/6',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
__typename: 'WorkItemType',
},
title: 'New title',
@@ -699,12 +790,16 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
{
type: 'HIERARCHY',
parent: null,
+ hasChildren: true,
children: {
nodes: [
{
id: 'gid://gitlab/WorkItem/2',
+ iid: '2',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
__typename: 'WorkItemType',
},
title: 'xyz',
@@ -712,6 +807,12 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ hasChildren: false,
+ },
+ ],
__typename: 'WorkItem',
},
],
@@ -727,8 +828,11 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
export const workItemTask = {
id: 'gid://gitlab/WorkItem/4',
+ iid: '4',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
__typename: 'WorkItemType',
},
title: 'bar',
@@ -741,8 +845,11 @@ export const workItemTask = {
export const confidentialWorkItemTask = {
id: 'gid://gitlab/WorkItem/2',
+ iid: '2',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
__typename: 'WorkItemType',
},
title: 'xyz',
@@ -755,8 +862,11 @@ export const confidentialWorkItemTask = {
export const closedWorkItemTask = {
id: 'gid://gitlab/WorkItem/3',
+ iid: '3',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
__typename: 'WorkItemType',
},
title: 'abc',
@@ -767,12 +877,153 @@ export const closedWorkItemTask = {
__typename: 'WorkItem',
};
+export const childrenWorkItems = [
+ confidentialWorkItemTask,
+ closedWorkItemTask,
+ workItemTask,
+ {
+ id: 'gid://gitlab/WorkItem/5',
+ iid: '5',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ __typename: 'WorkItemType',
+ },
+ title: 'foobar',
+ state: 'OPEN',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ __typename: 'WorkItem',
+ },
+];
+
export const workItemHierarchyResponse = {
data: {
workItem: {
id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/6',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+ __typename: 'WorkItemType',
+ },
+ title: 'New title',
+ userPermissions: {
+ deleteWorkItem: true,
+ updateWorkItem: true,
+ },
+ confidential: false,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
+ widgets: [
+ {
+ type: 'DESCRIPTION',
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ type: 'HIERARCHY',
+ parent: null,
+ hasChildren: true,
+ children: {
+ nodes: childrenWorkItems,
+ __typename: 'WorkItemConnection',
+ },
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ },
+};
+
+export const workItemObjectiveWithChild = {
+ id: 'gid://gitlab/WorkItem/12',
+ iid: '12',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2411',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+ __typename: 'WorkItemType',
+ },
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
+ userPermissions: {
+ deleteWorkItem: true,
+ updateWorkItem: true,
+ },
+ title: 'Objective',
+ description: 'Objective description',
+ state: 'OPEN',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ hasChildren: true,
+ parent: null,
+ children: {
+ nodes: [],
+ },
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ {
+ type: 'MILESTONE',
+ __typename: 'WorkItemWidgetMilestone',
+ milestone: mockMilestone,
+ },
+ {
+ type: 'ASSIGNEES',
+ __typename: 'WorkItemWidgetAssignees',
+ canInviteMembers: true,
+ allowsMultipleAssignees: true,
+ assignees: {
+ __typename: 'UserCoreConnection',
+ nodes: mockAssignees,
+ },
+ },
+ {
+ type: 'LABELS',
+ __typename: 'WorkItemWidgetLabels',
+ allowsScopedLabels: true,
+ labels: {
+ __typename: 'LabelConnection',
+ nodes: mockLabels,
+ },
+ },
+ ],
+ __typename: 'WorkItem',
+};
+
+export const workItemObjectiveNoMetadata = {
+ ...workItemObjectiveWithChild,
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ hasChildren: true,
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
+};
+
+export const workItemHierarchyTreeResponse = {
+ data: {
+ workItem: {
+ id: 'gid://gitlab/WorkItem/2',
+ iid: '2',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2411',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
__typename: 'WorkItemType',
},
title: 'New title',
@@ -794,22 +1045,30 @@ export const workItemHierarchyResponse = {
{
type: 'HIERARCHY',
parent: null,
+ hasChildren: true,
children: {
nodes: [
- confidentialWorkItemTask,
- closedWorkItemTask,
- workItemTask,
{
- id: 'gid://gitlab/WorkItem/5',
+ id: 'gid://gitlab/WorkItem/13',
+ iid: '13',
workItemType: {
- id: 'gid://gitlab/WorkItems::Type/5',
+ id: 'gid://gitlab/WorkItems::Type/2411',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
__typename: 'WorkItemType',
},
- title: 'foobar',
+ title: 'Objective 2',
state: 'OPEN',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ hasChildren: true,
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
__typename: 'WorkItem',
},
],
@@ -823,6 +1082,15 @@ export const workItemHierarchyResponse = {
},
};
+export const workItemHierarchyTreeFailureResponse = {
+ data: {},
+ errors: [
+ {
+ message: 'Something went wrong',
+ },
+ ],
+};
+
export const changeWorkItemParentMutationResponse = {
data: {
workItemUpdate: {
@@ -856,6 +1124,7 @@ export const changeWorkItemParentMutationResponse = {
__typename: 'WorkItemWidgetHierarchy',
type: 'HIERARCHY',
parent: null,
+ hasChildren: false,
children: {
nodes: [],
},
@@ -1196,3 +1465,288 @@ export const projectWorkItemResponse = {
},
},
};
+
+export const mockWorkItemNotesResponse = {
+ data: {
+ workItem: {
+ id: 'gid://gitlab/WorkItem/600',
+ iid: '60',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetIteration',
+ },
+ {
+ __typename: 'WorkItemWidgetWeight',
+ },
+ {
+ __typename: 'WorkItemWidgetAssignees',
+ },
+ {
+ __typename: 'WorkItemWidgetLabels',
+ },
+ {
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ {
+ __typename: 'WorkItemWidgetStartAndDueDate',
+ },
+ {
+ __typename: 'WorkItemWidgetMilestone',
+ },
+ {
+ type: 'NOTES',
+ discussions: {
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: null,
+ __typename: 'PageInfo',
+ },
+ nodes: [
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/2428',
+ body: 'added #31 as parent issue',
+ bodyHtml:
+ '<p data-sourcepos="1:1-1:25" dir="auto">added <a href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container=body data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue">#31</a> as parent issue</p>',
+ systemNoteIconName: 'link',
+ createdAt: '2022-11-14T04:18:59Z',
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/MilestoneNote/not-persisted',
+ body: 'changed milestone to %5',
+ bodyHtml:
+ '<p data-sourcepos="1:1-1:23" dir="auto">changed milestone to <a href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container=body data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a></p>',
+ systemNoteIconName: 'clock',
+ createdAt: '2022-11-14T04:18:59Z',
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WeightNote/not-persisted',
+ body: 'changed weight to 89',
+ bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
+ systemNoteIconName: 'weight',
+ createdAt: '2022-11-25T07:16:20Z',
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ ],
+ __typename: 'DiscussionConnection',
+ },
+ __typename: 'WorkItemWidgetNotes',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ },
+};
+export const mockWorkItemNotesByIidResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/6',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/600',
+ iid: '51',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetIteration',
+ },
+ {
+ __typename: 'WorkItemWidgetWeight',
+ },
+ {
+ __typename: 'WorkItemWidgetHealthStatus',
+ },
+ {
+ __typename: 'WorkItemWidgetAssignees',
+ },
+ {
+ __typename: 'WorkItemWidgetLabels',
+ },
+ {
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ {
+ __typename: 'WorkItemWidgetStartAndDueDate',
+ },
+ {
+ __typename: 'WorkItemWidgetMilestone',
+ },
+ {
+ type: 'NOTES',
+ discussions: {
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0xNCAwNDoxOTowMC4wOTkxMTcwMDAgKzAwMDAiLCJpZCI6IjQyNyIsIl9rZCI6Im4ifQ==',
+ __typename: 'PageInfo',
+ },
+ nodes: [
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/2428',
+ body: 'added #31 as parent issue',
+ bodyHtml:
+ '\u003cp data-sourcepos="1:1-1:25" dir="auto"\u003eadded \u003ca href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container="body" data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue"\u003e#31\u003c/a\u003e as parent issue\u003c/p\u003e',
+ systemNoteIconName: 'link',
+ createdAt: '2022-11-14T04:18:59Z',
+ 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',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ notes: {
+ nodes: [
+ {
+ id:
+ 'gid://gitlab/MilestoneNote/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ body: 'changed milestone to %5',
+ bodyHtml:
+ '\u003cp data-sourcepos="1:1-1:23" dir="auto"\u003echanged milestone to \u003ca href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip"\u003e%v4.0\u003c/a\u003e\u003c/p\u003e',
+ systemNoteIconName: 'clock',
+ createdAt: '2022-11-14T04:18:59Z',
+ 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',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/addbc177f7664699a135130ab05ffb78c57e4db3',
+ notes: {
+ nodes: [
+ {
+ id:
+ 'gid://gitlab/IterationNote/addbc177f7664699a135130ab05ffb78c57e4db3',
+ body: 'changed iteration to *iteration:5352',
+ bodyHtml:
+ '\u003cp data-sourcepos="1:1-1:36" dir="auto"\u003echanged iteration to \u003ca href="/groups/flightjs/-/iterations/5352" data-reference-type="iteration" data-original="*iteration:5352" data-link="false" data-link-reference="false" data-project="6" data-iteration="5352" data-container="body" data-placement="top" title="Iteration" class="gfm gfm-iteration has-tooltip"\u003eEt autem debitis nam suscipit eos ut. Jul 13, 2022 - Jul 19, 2022\u003c/a\u003e\u003c/p\u003e',
+ systemNoteIconName: 'iteration',
+ createdAt: '2022-11-14T04:19:00Z',
+ 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',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ ],
+ __typename: 'DiscussionConnection',
+ },
+ __typename: 'WorkItemWidgetNotes',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ ],
+ __typename: 'WorkItemConnection',
+ },
+ __typename: 'Project',
+ },
+ },
+};
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index 880c4271024..a766962771a 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -55,7 +55,7 @@ describe('Work items root component', () => {
isModal: false,
workItemId: 'gid://gitlab/WorkItem/1',
workItemParentId: null,
- iid: '1',
+ workItemIid: '1',
});
});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 982f9f71f9e..b503d819435 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -1,14 +1,12 @@
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
workItemAssigneesSubscriptionResponse,
workItemDatesSubscriptionResponse,
workItemResponseFactory,
workItemTitleSubscriptionResponse,
- workItemWeightSubscriptionResponse,
workItemLabelsSubscriptionResponse,
workItemMilestoneSubscriptionResponse,
workItemDescriptionSubscriptionResponse,
@@ -25,6 +23,8 @@ import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { createRouter } from '~/work_items/router';
+jest.mock('~/behaviors/markdown/render_gfm');
+
describe('Work items router', () => {
let wrapper;
@@ -33,7 +33,6 @@ describe('Work items router', () => {
const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory());
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
- const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
const assigneesSubscriptionHandler = jest
.fn()
.mockResolvedValue(workItemAssigneesSubscriptionResponse);
@@ -61,10 +60,6 @@ describe('Work items router', () => {
[workItemDescriptionSubscription, descriptionSubscriptionHandler],
];
- if (IS_EE) {
- handlers.push([workItemWeightSubscription, weightSubscriptionHandler]);
- }
-
wrapper = mount(App, {
apolloProvider: createMockApollo(handlers),
router,
@@ -72,6 +67,13 @@ describe('Work items router', () => {
fullPath: 'full-path',
issuesListPath: 'full-path/-/issues',
hasIssueWeightsFeature: false,
+ hasIterationsFeature: false,
+ hasOkrsFeature: false,
+ hasIssuableHealthStatusFeature: false,
+ },
+ stubs: {
+ WorkItemWeight: true,
+ WorkItemIteration: true,
},
});
};
diff --git a/spec/frontend_integration/content_editor/content_editor_integration_spec.js b/spec/frontend_integration/content_editor/content_editor_integration_spec.js
index 2fa491196ff..8521e85a971 100644
--- a/spec/frontend_integration/content_editor/content_editor_integration_spec.js
+++ b/spec/frontend_integration/content_editor/content_editor_integration_spec.js
@@ -114,8 +114,6 @@ This reference tag is a mix of letters and numbers [^footnote].
});
it('renders table of contents', async () => {
- jest.useFakeTimers();
-
renderMarkdown.mockResolvedValueOnce(`
<ul class="section-nav">
</ul>
diff --git a/spec/graphql/graphql_triggers_spec.rb b/spec/graphql/graphql_triggers_spec.rb
index a54cb8a7988..00b5aec366e 100644
--- a/spec/graphql/graphql_triggers_spec.rb
+++ b/spec/graphql/graphql_triggers_spec.rb
@@ -116,4 +116,18 @@ RSpec.describe GraphqlTriggers do
GraphqlTriggers.merge_request_merge_status_updated(merge_request)
end
end
+
+ describe '.merge_request_approval_state_updated' do
+ it 'triggers the mergeRequestApprovalStateUpdated subscription' do
+ merge_request = build_stubbed(:merge_request)
+
+ expect(GitlabSchema.subscriptions).to receive(:trigger).with(
+ 'mergeRequestApprovalStateUpdated',
+ { issuable_id: merge_request.to_gid },
+ merge_request
+ ).and_call_original
+
+ GraphqlTriggers.merge_request_approval_state_updated(merge_request)
+ end
+ end
end
diff --git a/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb b/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb
index 31abbabe385..125e15b70cf 100644
--- a/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb
+++ b/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb
@@ -56,6 +56,15 @@ RSpec.describe Mutations::AlertManagement::Alerts::SetAssignees do
context 'when operation mode is not specified' do
it_behaves_like 'successful resolution'
it_behaves_like 'an incident management tracked event', :incident_management_alert_assigned
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { project.namespace.reload }
+ let(:category) { described_class.to_s }
+ let(:user) { current_user }
+ let(:action) { 'incident_management_alert_assigned' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
end
context 'when user does not have permission to update alerts' do
diff --git a/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb b/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb
index ea5e21ec4b8..bcb7c74fa09 100644
--- a/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb
+++ b/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb
@@ -19,6 +19,15 @@ RSpec.describe Mutations::AlertManagement::Alerts::Todo::Create do
it_behaves_like 'an incident management tracked event', :incident_management_alert_todo
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { project.namespace.reload }
+ let(:category) { described_class.to_s }
+ let(:user) { current_user }
+ let(:action) { 'incident_management_alert_todo' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
+
context 'when user does not have permissions' do
let(:current_user) { nil }
diff --git a/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb b/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb
index 4758ac526a5..e49596b37c9 100644
--- a/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb
+++ b/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb
@@ -30,6 +30,15 @@ RSpec.describe Mutations::AlertManagement::CreateAlertIssue do
it_behaves_like 'an incident management tracked event', :incident_management_incident_created
it_behaves_like 'an incident management tracked event', :incident_management_alert_create_incident
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { project.namespace.reload }
+ let(:category) { described_class.to_s }
+ let(:user) { current_user }
+ let(:action) { 'incident_management_incident_created' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
end
context 'when CreateAlertIssue responds with an error' do
@@ -46,6 +55,15 @@ RSpec.describe Mutations::AlertManagement::CreateAlertIssue do
errors: ['An issue already exists']
)
end
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { project.namespace.reload }
+ let(:category) { described_class.to_s }
+ let(:user) { current_user }
+ let(:action) { 'incident_management_incident_created' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
end
end
diff --git a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
index 2c2518e046a..22ad93df79b 100644
--- a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
+++ b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
@@ -35,6 +35,15 @@ RSpec.describe Mutations::AlertManagement::UpdateAlertStatus do
let(:user) { current_user }
end
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { project.namespace }
+ let(:category) { described_class.to_s }
+ let(:user) { current_user }
+ let(:action) { 'incident_management_alert_status_changed' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
+
context 'error occurs when updating' do
it 'returns the alert with errors' do
# Stub an error on the alert
diff --git a/spec/graphql/mutations/ci/runner/bulk_delete_spec.rb b/spec/graphql/mutations/ci/runner/bulk_delete_spec.rb
index 2eccfd3409f..aaa74fa78aa 100644
--- a/spec/graphql/mutations/ci/runner/bulk_delete_spec.rb
+++ b/spec/graphql/mutations/ci/runner/bulk_delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::Ci::Runner::BulkDelete do
+RSpec.describe Mutations::Ci::Runner::BulkDelete, feature_category: :runner_fleet do
include GraphqlHelpers
let_it_be(:admin_user) { create(:user, :admin) }
diff --git a/spec/graphql/mutations/ci/runner/delete_spec.rb b/spec/graphql/mutations/ci/runner/delete_spec.rb
index 06d360430f8..f19fa7c34a9 100644
--- a/spec/graphql/mutations/ci/runner/delete_spec.rb
+++ b/spec/graphql/mutations/ci/runner/delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::Ci::Runner::Delete do
+RSpec.describe Mutations::Ci::Runner::Delete, feature_category: :runner_fleet do
include GraphqlHelpers
let_it_be(:runner) { create(:ci_runner) }
diff --git a/spec/graphql/mutations/ci/runner/update_spec.rb b/spec/graphql/mutations/ci/runner/update_spec.rb
index 098b7ac6aa4..e0c8219e0f6 100644
--- a/spec/graphql/mutations/ci/runner/update_spec.rb
+++ b/spec/graphql/mutations/ci/runner/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::Ci::Runner::Update do
+RSpec.describe Mutations::Ci::Runner::Update, feature_category: :runner_fleet do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/graphql/mutations/container_repositories/destroy_spec.rb b/spec/graphql/mutations/container_repositories/destroy_spec.rb
index 9f3ff8da80b..50e83ccdd30 100644
--- a/spec/graphql/mutations/container_repositories/destroy_spec.rb
+++ b/spec/graphql/mutations/container_repositories/destroy_spec.rb
@@ -55,23 +55,6 @@ RSpec.describe Mutations::ContainerRepositories::Destroy do
it_behaves_like params[:shared_examples_name]
end
-
- context 'with container_registry_delete_repository_with_cron_worker disabled' do
- before do
- project.add_maintainer(user)
- stub_feature_flags(container_registry_delete_repository_with_cron_worker: false)
- end
-
- it 'enqueues a removal job' do
- expect(::Packages::CreateEventService)
- .to receive(:new).with(nil, user, event_name: :delete_repository, scope: :container).and_call_original
- expect(DeleteContainerRepositoryWorker)
- .to receive(:perform_async).with(user.id, container_repository.id)
-
- expect { subject }.to change { ::Packages::Event.count }.by(1)
- expect(container_repository.reload.delete_scheduled?).to be true
- end
- end
end
end
end
diff --git a/spec/graphql/mutations/incident_management/timeline_event/update_spec.rb b/spec/graphql/mutations/incident_management/timeline_event/update_spec.rb
index 7081fb7117e..317e8f5fcb6 100644
--- a/spec/graphql/mutations/incident_management/timeline_event/update_spec.rb
+++ b/spec/graphql/mutations/incident_management/timeline_event/update_spec.rb
@@ -7,21 +7,33 @@ RSpec.describe Mutations::IncidentManagement::TimelineEvent::Update do
let_it_be(:reporter) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:incident) { create(:incident, project: project) }
+ let_it_be(:tag1) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 1') }
+ let_it_be(:tag2) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 2') }
let_it_be_with_reload(:timeline_event) do
create(:incident_management_timeline_event, project: project, incident: incident)
end
+ # Pre-attach a tag to the event
+ let_it_be(:tag_link1) do
+ create(:incident_management_timeline_event_tag_link,
+ timeline_event: timeline_event,
+ timeline_event_tag: tag1
+ )
+ end
+
let(:args) do
{
id: timeline_event_id,
note: note,
- occurred_at: occurred_at
+ occurred_at: occurred_at,
+ timeline_event_tag_names: tag_names
}
end
let(:note) { 'Updated Note' }
let(:timeline_event_id) { GitlabSchema.id_from_object(timeline_event).to_s }
let(:occurred_at) { 1.minute.ago }
+ let(:tag_names) { [] }
before do
project.add_developer(developer)
@@ -92,6 +104,36 @@ RSpec.describe Mutations::IncidentManagement::TimelineEvent::Update do
expect(resolve).to eq(timeline_event: nil, errors: ["Occurred at can't be blank"])
end
end
+
+ context 'when timeline event tag do not exist' do
+ let(:tag_names) { ['some other tag'] }
+
+ it 'does not update the timeline event' do
+ expect { resolve }.not_to change { timeline_event.reload.updated_at }
+ end
+
+ it 'responds with error' do
+ expect(resolve).to eq(timeline_event: nil, errors: ["Following tags don't exist: [\"some other tag\"]"])
+ end
+ end
+ end
+
+ context 'when timeline event tags are passed' do
+ let(:tag_names) { [tag2.name] }
+
+ it 'returns updated timeline event' do
+ expect(resolve).to eq(
+ timeline_event: timeline_event.reload,
+ errors: []
+ )
+ end
+
+ it 'removes tag1 and assigns tag2 to the event' do
+ response = resolve
+ timeline_event = response[:timeline_event]
+
+ expect(timeline_event.timeline_event_tags).to contain_exactly(tag2)
+ end
end
end
diff --git a/spec/graphql/mutations/issues/link_alerts_spec.rb b/spec/graphql/mutations/issues/link_alerts_spec.rb
new file mode 100644
index 00000000000..a6ce5cdd7ab
--- /dev/null
+++ b/spec/graphql/mutations/issues/link_alerts_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Issues::LinkAlerts, feature_category: :incident_management do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:issue) { create(:incident, project: project) }
+ let_it_be(:alert1) { create(:alert_management_alert, project: project) }
+ let_it_be(:alert2) { create(:alert_management_alert, project: project) }
+
+ let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+
+ specify { expect(described_class).to require_graphql_authorizations(:update_issue, :admin_issue) }
+
+ before_all do
+ project.add_guest(guest)
+ project.add_developer(developer)
+ end
+
+ describe '#resolve' do
+ let(:alert_references) { [alert1.to_reference, alert2.details_url, 'invalid-reference'] }
+
+ subject(:resolve) do
+ mutation.resolve(
+ project_path: issue.project.full_path,
+ iid: issue.iid,
+ alert_references: alert_references
+ )
+ end
+
+ context 'when the user is a guest' do
+ let(:user) { guest }
+
+ it 'raises an error' do
+ expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+
+ context 'when a user is also an author' do
+ let!(:issue) { create(:incident, project: project, author: user) }
+
+ it 'raises an error' do
+ expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when a user is also an assignee' do
+ let!(:issue) { create(:incident, project: project, assignee_ids: [user.id]) }
+
+ it 'raises an error' do
+ expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+
+ context 'when the user is a developer' do
+ let(:user) { developer }
+
+ context 'when issue type is an incident' do
+ it 'calls LinkAlerts::CreateService with correct arguments' do
+ expect(::IncidentManagement::LinkAlerts::CreateService)
+ .to receive(:new)
+ .with(issue, user, alert_references)
+ .and_call_original
+
+ resolve
+ end
+
+ it 'returns no errors' do
+ expect(resolve[:errors]).to be_empty
+ end
+ end
+
+ context 'when issue type is not an incident' do
+ let!(:issue) { create(:issue, project: project) }
+
+ it 'does not update alert_management_alerts' do
+ expect { resolve }.not_to change { issue.alert_management_alerts }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/issues/unlink_alert_spec.rb b/spec/graphql/mutations/issues/unlink_alert_spec.rb
new file mode 100644
index 00000000000..2f1d5084faf
--- /dev/null
+++ b/spec/graphql/mutations/issues/unlink_alert_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Issues::UnlinkAlert, feature_category: :incident_management do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:another_project) { create(:project) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:internal_alert) { create(:alert_management_alert, project: project) }
+ let_it_be(:external_alert) { create(:alert_management_alert, project: another_project) }
+ let_it_be(:issue) { create(:incident, project: project, alert_management_alerts: [internal_alert, external_alert]) }
+
+ let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+
+ specify { expect(described_class).to require_graphql_authorizations(:update_issue, :admin_issue) }
+
+ before_all do
+ project.add_guest(guest)
+ project.add_developer(developer)
+ end
+
+ describe '#resolve' do
+ let(:alert_to_unlink) { internal_alert }
+
+ subject(:resolve) do
+ mutation.resolve(
+ project_path: issue.project.full_path,
+ iid: issue.iid,
+ alert_id: alert_to_unlink.to_global_id.to_s
+ )
+ end
+
+ context 'when the user is a guest' do
+ let(:user) { guest }
+
+ it 'raises an error' do
+ expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when the user is a developer' do
+ let(:user) { developer }
+
+ shared_examples 'unlinking an alert' do
+ it 'unlinks the alert' do
+ expect { resolve }.to change { issue.reload.alert_management_alerts }.to match_array(remainded_alerts)
+ end
+
+ it 'returns no errors' do
+ expect(resolve[:errors]).to be_empty
+ end
+ end
+
+ context 'when unlinking internal alert' do
+ let(:alert_to_unlink) { internal_alert }
+ let(:remainded_alerts) { [external_alert] }
+
+ it_behaves_like 'unlinking an alert'
+ end
+
+ context 'when unlinking external alert' do
+ let(:alert_to_unlink) { external_alert }
+ let(:remainded_alerts) { [internal_alert] }
+
+ it_behaves_like 'unlinking an alert'
+ end
+
+ context 'when LinkAlerts::DestroyService responds with an error' do
+ it 'returns the error' do
+ service_instance = instance_double(
+ ::IncidentManagement::LinkAlerts::DestroyService,
+ execute: ServiceResponse.error(message: 'some error message')
+ )
+
+ allow(::IncidentManagement::LinkAlerts::DestroyService).to receive(:new).and_return(service_instance)
+
+ expect(resolve[:errors]).to match_array(['some error message'])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb b/spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb
index 2a7d0a8171b..5c632ed3443 100644
--- a/spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb
@@ -11,29 +11,46 @@ RSpec.describe Resolvers::Ci::AllJobsResolver do
let_it_be(:pending_job) { create(:ci_build, :pending, name: 'Job Three') }
let(:args) { {} }
- let(:current_user) { create(:admin) }
subject { resolve_jobs(args) }
describe '#resolve' do
- context 'with authorized user' do
- context 'with statuses argument' do
- let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS')] } }
+ context 'with admin' do
+ let(:current_user) { create(:admin) }
- it { is_expected.to contain_exactly(successful_job, successful_job_two) }
- end
+ shared_examples 'executes as admin' do
+ context 'with statuses argument' do
+ let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS')] } }
+
+ it { is_expected.to contain_exactly(successful_job, successful_job_two) }
+ end
+
+ context 'with multiple statuses' do
+ let(:args) do
+ { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS'),
+ Types::Ci::JobStatusEnum.coerce_isolated_input('FAILED')] }
+ end
+
+ it { is_expected.to contain_exactly(successful_job, successful_job_two, failed_job) }
+ end
- context 'with multiple statuses' do
- let(:args) do
- { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS'),
- Types::Ci::JobStatusEnum.coerce_isolated_input('FAILED')] }
+ context 'without statuses argument' do
+ it { is_expected.to contain_exactly(successful_job, successful_job_two, failed_job, pending_job) }
end
+ end
- it { is_expected.to contain_exactly(successful_job, successful_job_two, failed_job) }
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it_behaves_like 'executes as admin'
end
- context 'without statuses argument' do
- it { is_expected.to contain_exactly(successful_job, successful_job_two, failed_job, pending_job) }
+ context 'when admin mode setting is enabled' do
+ context 'when in admin mode', :enable_admin_mode do
+ it_behaves_like 'executes as admin'
+ end
+
+ context 'when not in admin mode' do
+ it { is_expected.to be_empty }
+ end
end
end
diff --git a/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb b/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
index 57b2fcbea63..5d06db904d5 100644
--- a/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::Ci::GroupRunnersResolver do
+RSpec.describe Resolvers::Ci::GroupRunnersResolver, feature_category: :runner_fleet do
include GraphqlHelpers
describe '#resolve' do
@@ -78,7 +78,7 @@ RSpec.describe Resolvers::Ci::GroupRunnersResolver do
status_status: 'active',
type_type: :group_type,
tag_name: ['active_runner'],
- preload: { tag_name: nil },
+ preload: { tag_name: false },
search: 'abc',
sort: 'contacted_asc',
membership: :descendants,
diff --git a/spec/graphql/resolvers/ci/project_runners_resolver_spec.rb b/spec/graphql/resolvers/ci/project_runners_resolver_spec.rb
new file mode 100644
index 00000000000..4cc00ced104
--- /dev/null
+++ b/spec/graphql/resolvers/ci/project_runners_resolver_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::ProjectRunnersResolver, feature_category: :runner_fleet do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ subject do
+ resolve(described_class, obj: obj, ctx: { current_user: user }, args: args,
+ arg_style: :internal)
+ end
+
+ include_context 'runners resolver setup'
+
+ let(:obj) { project }
+ let(:args) { {} }
+
+ context 'when user cannot see runners' do
+ it 'returns no runners' do
+ expect(subject.items.to_a).to eq([])
+ end
+ end
+
+ context 'with user as project admin' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ let(:available_runners) { [inactive_project_runner, offline_project_runner, group_runner, instance_runner] }
+
+ it 'returns all runners available to the project' do
+ expect(subject.items.to_a).to match_array(available_runners)
+ end
+ end
+
+ context 'with obj set to nil' do
+ let(:obj) { nil }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error('Expected project missing')
+ end
+ end
+
+ context 'with obj not set to project' do
+ let(:obj) { build(:group) }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error('Expected project missing')
+ end
+ end
+
+ describe 'Allowed query arguments' do
+ let(:finder) { instance_double(::Ci::RunnersFinder) }
+ let(:args) do
+ {
+ status: 'active',
+ type: :group_type,
+ tag_list: ['active_runner'],
+ search: 'abc',
+ sort: :contacted_asc
+ }
+ end
+
+ let(:expected_params) do
+ {
+ status_status: 'active',
+ type_type: :group_type,
+ tag_name: ['active_runner'],
+ preload: { tag_name: false },
+ search: 'abc',
+ sort: 'contacted_asc',
+ project: project
+ }
+ end
+
+ it 'calls RunnersFinder with expected arguments' do
+ allow(::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(subject.items.to_a).to eq([:execute_return_value])
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/ci/runner_groups_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_groups_resolver_spec.rb
new file mode 100644
index 00000000000..9272689ef0b
--- /dev/null
+++ b/spec/graphql/resolvers/ci/runner_groups_resolver_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::RunnerGroupsResolver, feature_category: :runner_fleet do
+ include GraphqlHelpers
+
+ let_it_be(:group1) { create(:group) }
+ let_it_be(:runner) { create(:ci_runner, :group, groups: [group1]) }
+
+ let(:args) { {} }
+
+ subject(:response) { resolve_groups(args) }
+
+ describe '#resolve' do
+ context 'with authorized user', :enable_admin_mode do
+ let(:current_user) { create(:user, :admin) }
+
+ it 'returns a lazy value with all groups' do
+ expect(response).to be_a(GraphQL::Execution::Lazy)
+ expect(response.value).to contain_exactly(group1)
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ private
+
+ def resolve_groups(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: runner, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb
index ba8a127bec5..963a642fa4e 100644
--- a/spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runner_jobs_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::Ci::RunnerJobsResolver do
+RSpec.describe Resolvers::Ci::RunnerJobsResolver, feature_category: :runner_fleet do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb
index 3cb6e94e81e..1d1fb4a9967 100644
--- a/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::Ci::RunnerPlatformsResolver do
+RSpec.describe Resolvers::Ci::RunnerPlatformsResolver, feature_category: :runner_fleet do
include GraphqlHelpers
describe '#resolve' do
diff --git a/spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb
index 952c7337d65..6c69cdc19cc 100644
--- a/spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::Ci::RunnerProjectsResolver do
+RSpec.describe Resolvers::Ci::RunnerProjectsResolver, feature_category: :runner_fleet do
include GraphqlHelpers
let_it_be(:project1) { create(:project, description: 'Project1.1') }
diff --git a/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb
index 13ef89023d9..734337f7c92 100644
--- a/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::Ci::RunnerSetupResolver do
+RSpec.describe Resolvers::Ci::RunnerSetupResolver, feature_category: :runner_fleet do
include GraphqlHelpers
describe '#resolve' do
diff --git a/spec/graphql/resolvers/ci/runner_status_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_status_resolver_spec.rb
index fbef07b72e6..2bea256856d 100644
--- a/spec/graphql/resolvers/ci/runner_status_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runner_status_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::Ci::RunnerStatusResolver do
+RSpec.describe Resolvers::Ci::RunnerStatusResolver, feature_category: :runner_fleet do
include GraphqlHelpers
describe '#resolve' do
diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
index 4038192a68a..d6da8222234 100644
--- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::Ci::RunnersResolver do
+RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet do
include GraphqlHelpers
describe '#resolve' do
@@ -28,8 +28,24 @@ RSpec.describe Resolvers::Ci::RunnersResolver do
context 'when user can see runners' do
let(:obj) { nil }
- it 'returns all the runners' do
- expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner, instance_runner)
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it 'returns all the runners' do
+ expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner, instance_runner)
+ end
+ end
+
+ context 'when admin mode setting is enabled' do
+ context 'when in admin mode', :enable_admin_mode do
+ it 'returns all the runners' do
+ expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner, instance_runner)
+ end
+ end
+
+ context 'when not in admin mode' do
+ it 'returns no runners' do
+ expect(subject.items.to_a).to eq([])
+ end
+ end
end
end
@@ -67,7 +83,7 @@ RSpec.describe Resolvers::Ci::RunnersResolver do
upgrade_status: 'recommended',
type_type: :instance_type,
tag_name: ['active_runner'],
- preload: { tag_name: nil },
+ preload: { tag_name: false },
search: 'abc',
sort: 'contacted_asc'
}
@@ -92,7 +108,7 @@ RSpec.describe Resolvers::Ci::RunnersResolver do
let(:expected_params) do
{
active: false,
- preload: { tag_name: nil }
+ preload: { tag_name: false }
}
end
@@ -112,7 +128,7 @@ RSpec.describe Resolvers::Ci::RunnersResolver do
let(:expected_params) do
{
active: false,
- preload: { tag_name: nil }
+ preload: { tag_name: false }
}
end
@@ -131,7 +147,7 @@ RSpec.describe Resolvers::Ci::RunnersResolver do
let(:expected_params) do
{
- preload: { tag_name: nil }
+ preload: { tag_name: false }
}
end
diff --git a/spec/graphql/resolvers/design_management/design_resolver_spec.rb b/spec/graphql/resolvers/design_management/design_resolver_spec.rb
index 0915dddf438..3a62f24993a 100644
--- a/spec/graphql/resolvers/design_management/design_resolver_spec.rb
+++ b/spec/graphql/resolvers/design_management/design_resolver_spec.rb
@@ -6,14 +6,14 @@ RSpec.describe Resolvers::DesignManagement::DesignResolver do
include GraphqlHelpers
include DesignManagementTestHelpers
- specify do
- expect(described_class).to have_nullable_graphql_type(::Types::DesignManagement::DesignType)
- end
-
before do
enable_design_management
end
+ specify do
+ expect(described_class).to have_nullable_graphql_type(::Types::DesignManagement::DesignType)
+ end
+
describe '#resolve' do
let_it_be(:issue) { create(:issue) }
let_it_be(:project) { issue.project }
diff --git a/spec/graphql/resolvers/design_management/designs_resolver_spec.rb b/spec/graphql/resolvers/design_management/designs_resolver_spec.rb
index 64eae14d888..7024e62c670 100644
--- a/spec/graphql/resolvers/design_management/designs_resolver_spec.rb
+++ b/spec/graphql/resolvers/design_management/designs_resolver_spec.rb
@@ -6,14 +6,14 @@ RSpec.describe Resolvers::DesignManagement::DesignsResolver do
include GraphqlHelpers
include DesignManagementTestHelpers
- specify do
- expect(described_class).to have_nullable_graphql_type(::Types::DesignManagement::DesignType.connection_type)
- end
-
before do
enable_design_management
end
+ specify do
+ expect(described_class).to have_nullable_graphql_type(::Types::DesignManagement::DesignType.connection_type)
+ end
+
describe '#resolve' do
let_it_be(:issue) { create(:issue) }
let_it_be(:project) { issue.project }
diff --git a/spec/graphql/resolvers/environments/nested_environments_resolver_spec.rb b/spec/graphql/resolvers/environments/nested_environments_resolver_spec.rb
new file mode 100644
index 00000000000..07197b1838f
--- /dev/null
+++ b/spec/graphql/resolvers/environments/nested_environments_resolver_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Environments::NestedEnvironmentsResolver, feature_category: :continuous_delivery do
+ include GraphqlHelpers
+ include Gitlab::Graphql::Laziness
+
+ let_it_be(:project) { create(:project, :repository, :private) }
+ let_it_be(:environment) { create(:environment, project: project, name: 'test') }
+ let_it_be(:environment2) { create(:environment, project: project, name: 'folder1/test') }
+ let_it_be(:environment3) { create(:environment, project: project, name: 'folder1/test2') }
+ let_it_be(:environment4) { create(:environment, project: project, name: 'folder2/test') }
+
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+
+ let(:current_user) { developer }
+
+ describe '#resolve' do
+ it 'finds the nested environments when status matches' do
+ expect(resolve_nested_environments(status: :created).to_a.pluck(:name, :size))
+ .to match_array([
+ ['test', 1],
+ ['folder1', 2],
+ ['folder2', 1]
+ ])
+ end
+
+ it 'finds the nested environments when searching by name' do
+ expect(resolve_nested_environments(search: 'folder2').to_a.pluck(:name, :size))
+ .to match_array([
+ ['folder2', 1]
+ ])
+ end
+
+ it 'finds the nested environments when name matches exactly' do
+ expect(resolve_nested_environments(name: 'test').to_a.pluck(:name, :size))
+ .to match_array([
+ ['test', 1]
+ ])
+ end
+ end
+
+ def resolve_nested_environments(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: project, ctx: context, args: args)
+ end
+end
diff --git a/spec/graphql/resolvers/environments_resolver_spec.rb b/spec/graphql/resolvers/environments_resolver_spec.rb
index 9f4c4716de0..b1f7dc1673e 100644
--- a/spec/graphql/resolvers/environments_resolver_spec.rb
+++ b/spec/graphql/resolvers/environments_resolver_spec.rb
@@ -13,6 +13,9 @@ RSpec.describe Resolvers::EnvironmentsResolver do
let!(:environment1) { create(:environment, :available, name: 'production', project: project) }
let!(:environment2) { create(:environment, :stopped, name: 'test', project: project) }
let!(:environment3) { create(:environment, :available, name: 'test2', project: project) }
+ let!(:environment4) { create(:environment, :available, name: 'folder1/test1', project: project) }
+ let!(:environment5) { create(:environment, :available, name: 'folder1/test2', project: project) }
+ let!(:environment6) { create(:environment, :available, name: 'folder2/test3', project: project) }
before do
group.add_developer(current_user)
@@ -20,7 +23,12 @@ RSpec.describe Resolvers::EnvironmentsResolver do
describe '#resolve' do
it 'finds all environments' do
- expect(resolve_environments).to contain_exactly(environment1, environment2, environment3)
+ expect(resolve_environments).to contain_exactly(environment1,
+ environment2,
+ environment3,
+ environment4,
+ environment5,
+ environment6)
end
context 'with name' do
@@ -31,7 +39,7 @@ RSpec.describe Resolvers::EnvironmentsResolver do
context 'with search' do
it 'searches environment by name' do
- expect(resolve_environments(search: 'test')).to contain_exactly(environment2, environment3)
+ expect(resolve_environments(search: 'production')).to contain_exactly(environment1)
end
context 'when the search term does not match any environments' do
@@ -43,7 +51,11 @@ RSpec.describe Resolvers::EnvironmentsResolver do
context 'with states' do
it 'searches environments by state' do
- expect(resolve_environments(states: ['available'])).to contain_exactly(environment1, environment3)
+ expect(resolve_environments(states: ['available'])).to contain_exactly(environment1,
+ environment3,
+ environment4,
+ environment5,
+ environment6)
end
it 'generates an error if requested state is invalid' do
@@ -53,6 +65,16 @@ RSpec.describe Resolvers::EnvironmentsResolver do
end
end
+ context 'with environment_type' do
+ it 'searches environments by type' do
+ expect(resolve_environments(type: 'folder1')).to contain_exactly(environment4, environment5)
+ end
+
+ it 'returns an empty result' do
+ expect(resolve_environments(type: 'folder3')).to be_empty
+ end
+ end
+
context 'when project is nil' do
subject { resolve(described_class, obj: nil, args: {}, ctx: { current_user: current_user }) }
diff --git a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb
index 78dd5173449..07ea98f00c7 100644
--- a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb
+++ b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::NamespaceProjectsResolver do
+RSpec.describe Resolvers::NamespaceProjectsResolver, feature_category: :subgroups do
include GraphqlHelpers
let(:current_user) { create(:user) }
diff --git a/spec/graphql/resolvers/paginated_tree_resolver_spec.rb b/spec/graphql/resolvers/paginated_tree_resolver_spec.rb
index 4b05e9076d7..9a04b716001 100644
--- a/spec/graphql/resolvers/paginated_tree_resolver_spec.rb
+++ b/spec/graphql/resolvers/paginated_tree_resolver_spec.rb
@@ -66,9 +66,8 @@ RSpec.describe Resolvers::PaginatedTreeResolver do
let(:args) { super().merge(after: 'invalid') }
it 'generates an error' do
- expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
- subject
- end
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::BaseError) { subject }
+ expect(subject.extensions.keys).to match_array([:code, :gitaly_code, :service])
end
end
@@ -92,6 +91,22 @@ RSpec.describe Resolvers::PaginatedTreeResolver do
expect(collected_entries).to match_array(expected_entries)
end
end
+
+ describe 'Custom error handling' do
+ before do
+ grpc_err = GRPC::Unavailable.new
+ allow(repository).to receive(:tree).and_raise(Gitlab::Git::CommandError, grpc_err)
+ end
+
+ context 'when gitaly is not available' do
+ let(:request) { get :index, format: :html, params: { namespace_id: project.namespace, project_id: project } }
+
+ it 'generates an unavailable error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::BaseError) { subject }
+ expect(subject.extensions).to eq(code: 'unavailable', gitaly_code: 14, service: 'git')
+ end
+ end
+ end
end
def resolve_repository(args, opts = {})
diff --git a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb
index 398f8f52269..9250485e4c8 100644
--- a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb
@@ -12,6 +12,10 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
let(:current_user) { create(:user) }
+ before do
+ project.add_developer(current_user)
+ end
+
specify do
expect(described_class).to have_nullable_graphql_type(::Types::Ci::PipelineType)
end
@@ -20,10 +24,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
resolve(described_class, obj: project, args: args, ctx: { current_user: current_user })
end
- before do
- project.add_developer(current_user)
- end
-
it 'resolves pipeline for the passed iid' do
expect(Ci::PipelinesFinder)
.to receive(:new)
diff --git a/spec/graphql/resolvers/users/participants_resolver_spec.rb b/spec/graphql/resolvers/users/participants_resolver_spec.rb
index eb2418b63f4..27c3b9643ce 100644
--- a/spec/graphql/resolvers/users/participants_resolver_spec.rb
+++ b/spec/graphql/resolvers/users/participants_resolver_spec.rb
@@ -115,7 +115,8 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do
create(:award_emoji, name: 'thumbsup', awardable: public_note)
# 1 extra query per source (3 emojis + 2 notes) to fetch participables collection
- expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(5)
+ # 1 extra query to load work item widgets collection
+ expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(6)
end
it 'does not execute N+1 for system note metadata relation' do
diff --git a/spec/graphql/types/alert_management/alert_type_spec.rb b/spec/graphql/types/alert_management/alert_type_spec.rb
index 69cbdb998eb..c1df24ccb5c 100644
--- a/spec/graphql/types/alert_management/alert_type_spec.rb
+++ b/spec/graphql/types/alert_management/alert_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['AlertManagementAlert'] do
+RSpec.describe GitlabSchema.types['AlertManagementAlert'], feature_category: :incident_management do
specify { expect(described_class.graphql_name).to eq('AlertManagementAlert') }
specify { expect(described_class).to require_graphql_authorizations(:read_alert_management_alert) }
@@ -11,6 +11,7 @@ RSpec.describe GitlabSchema.types['AlertManagementAlert'] do
it 'exposes the expected fields' do
expected_fields = %i[
+ id
iid
issueIid
issue
diff --git a/spec/graphql/types/ci/detailed_status_type_spec.rb b/spec/graphql/types/ci/detailed_status_type_spec.rb
index 686461cb9a5..81ab1b52552 100644
--- a/spec/graphql/types/ci/detailed_status_type_spec.rb
+++ b/spec/graphql/types/ci/detailed_status_type_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe Types::Ci::DetailedStatusType do
include GraphqlHelpers
+ let_it_be(:stage) { create(:ci_stage, status: :skipped) }
+
specify { expect(described_class.graphql_name).to eq('DetailedStatus') }
it 'has all fields' do
@@ -13,8 +15,6 @@ RSpec.describe Types::Ci::DetailedStatusType do
:label, :text, :tooltip, :action)
end
- let_it_be(:stage) { create(:ci_stage, status: :skipped) }
-
describe 'id field' do
it 'correctly renders the field' do
status = stage.detailed_status(stage.pipeline.user)
diff --git a/spec/graphql/types/ci/freeze_period_status_enum_spec.rb b/spec/graphql/types/ci/freeze_period_status_enum_spec.rb
new file mode 100644
index 00000000000..2d9c7071392
--- /dev/null
+++ b/spec/graphql/types/ci/freeze_period_status_enum_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiFreezePeriodStatus'], feature_category: :release_orchestration do
+ it 'exposes all freeze period statuses' do
+ expect(described_class.values.keys).to contain_exactly(*%w[ACTIVE INACTIVE])
+ end
+end
diff --git a/spec/graphql/types/ci/freeze_period_type_spec.rb b/spec/graphql/types/ci/freeze_period_type_spec.rb
new file mode 100644
index 00000000000..a7f7c2f9dc8
--- /dev/null
+++ b/spec/graphql/types/ci/freeze_period_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiFreezePeriod'], feature_category: :release_orchestration do
+ specify { expect(described_class.graphql_name).to eq('CiFreezePeriod') }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ status start_cron end_cron cron_timezone start_time end_time
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_freeze_period) }
+end
diff --git a/spec/graphql/types/ci/pipeline_counts_type_spec.rb b/spec/graphql/types/ci/pipeline_counts_type_spec.rb
index 7fdb286d253..6452827dc7b 100644
--- a/spec/graphql/types/ci/pipeline_counts_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_counts_type_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe GitlabSchema.types['PipelineCounts'] do
expect(described_class).to include_graphql_fields(*expected_fields)
end
- shared_examples 'pipeline counts query' do |args: "", expected_counts:|
+ shared_examples 'pipeline counts query' do |expected_counts:, args: ""|
let_it_be(:query) do
%(
query {
diff --git a/spec/graphql/types/ci/pipeline_schedule_type_spec.rb b/spec/graphql/types/ci/pipeline_schedule_type_spec.rb
index bf1413ef657..6e6c6c63969 100644
--- a/spec/graphql/types/ci/pipeline_schedule_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_schedule_type_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe Types::Ci::PipelineScheduleType do
description
owner
active
+ project
lastPipeline
refForDisplay
refPath
@@ -23,6 +24,13 @@ RSpec.describe Types::Ci::PipelineScheduleType do
cron
cronTimezone
userPermissions
+ editPath
+ cron
+ cronTimezone
+ ref
+ variables
+ createdAt
+ updatedAt
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/ci/pipeline_schedule_variable_type_spec.rb b/spec/graphql/types/ci/pipeline_schedule_variable_type_spec.rb
new file mode 100644
index 00000000000..1c98539e308
--- /dev/null
+++ b/spec/graphql/types/ci/pipeline_schedule_variable_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::PipelineScheduleVariableType do
+ specify { expect(described_class.graphql_name).to eq('PipelineScheduleVariable') }
+ specify { expect(described_class.interfaces).to contain_exactly(Types::Ci::VariableInterface) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_pipeline_schedule_variables) }
+
+ it 'contains attributes related to a pipeline message' do
+ expected_fields = %w[
+ id key raw value variable_type
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/runner_type_spec.rb b/spec/graphql/types/ci/runner_type_spec.rb
index 4ec35db13fb..b078d7f5fac 100644
--- a/spec/graphql/types/ci/runner_type_spec.rb
+++ b/spec/graphql/types/ci/runner_type_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe GitlabSchema.types['CiRunner'] do
id description created_at contacted_at maximum_timeout access_level active paused status
version short_sha revision locked run_untagged ip_address runner_type tag_list
project_count job_count admin_url edit_admin_url user_permissions executor_name architecture_name platform_name
- maintenance_note maintenance_note_html groups projects jobs token_expires_at owner_project
+ maintenance_note maintenance_note_html groups projects jobs token_expires_at owner_project job_execution_status
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/commit_signatures/ssh_signature_type_spec.rb b/spec/graphql/types/commit_signatures/ssh_signature_type_spec.rb
new file mode 100644
index 00000000000..4ffb70a0b22
--- /dev/null
+++ b/spec/graphql/types/commit_signatures/ssh_signature_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['SshSignature'], feature_category: :source_code_management do
+ specify { expect(described_class.graphql_name).to eq('SshSignature') }
+
+ specify { expect(described_class).to require_graphql_authorizations(:download_code) }
+
+ specify { expect(described_class).to include(Types::CommitSignatureInterface) }
+
+ it 'contains attributes related to SSH signatures' do
+ expect(described_class).to have_graphql_fields(
+ :user, :verification_status, :commit_sha, :project, :key
+ )
+ end
+end
diff --git a/spec/graphql/types/deployment_details_type_spec.rb b/spec/graphql/types/deployment_details_type_spec.rb
deleted file mode 100644
index 7dc0c8f97ac..00000000000
--- a/spec/graphql/types/deployment_details_type_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe GitlabSchema.types['DeploymentDetails'] do
- specify { expect(described_class.graphql_name).to eq('DeploymentDetails') }
-
- it 'has the expected fields' do
- expected_fields = %w[
- id iid ref tag tags sha created_at updated_at finished_at status commit job triggerer
- ]
-
- expect(described_class).to include_graphql_fields(*expected_fields)
- end
-
- specify { expect(described_class).to require_graphql_authorizations(:read_deployment) }
-end
diff --git a/spec/graphql/types/deployment_type_spec.rb b/spec/graphql/types/deployment_type_spec.rb
index bf4be0523c6..4c49391d7a8 100644
--- a/spec/graphql/types/deployment_type_spec.rb
+++ b/spec/graphql/types/deployment_type_spec.rb
@@ -2,15 +2,16 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['Deployment'] do
+RSpec.describe GitlabSchema.types['Deployment'], feature_category: :continuous_delivery do
specify { expect(described_class.graphql_name).to eq('Deployment') }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Deployment) }
it 'has the expected fields' do
expected_fields = %w[
- id iid ref tag sha created_at updated_at finished_at status commit job triggerer
+ id iid ref tag tags sha created_at updated_at finished_at status commit job triggerer userPermissions
]
- expect(described_class).to have_graphql_fields(*expected_fields)
+ expect(described_class).to include_graphql_fields(*expected_fields)
end
specify { expect(described_class).to require_graphql_authorizations(:read_deployment) }
diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb
index 2605beac95a..4471735876a 100644
--- a/spec/graphql/types/environment_type_spec.rb
+++ b/spec/graphql/types/environment_type_spec.rb
@@ -4,11 +4,12 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['Environment'] do
specify { expect(described_class.graphql_name).to eq('Environment') }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Environment) }
it 'includes the expected fields' do
expected_fields = %w[
name id state metrics_dashboard latest_opened_most_severe_alert path external_url deployments
- slug createdAt updatedAt autoStopAt autoDeleteAt tier environmentType lastDeployment
+ slug createdAt updatedAt autoStopAt autoDeleteAt tier environmentType lastDeployment deployFreezes
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/issue_type_enum_spec.rb b/spec/graphql/types/issue_type_enum_spec.rb
index cd1737c3ebb..33a3a9cf8ce 100644
--- a/spec/graphql/types/issue_type_enum_spec.rb
+++ b/spec/graphql/types/issue_type_enum_spec.rb
@@ -2,12 +2,12 @@
require 'spec_helper'
-RSpec.describe Types::IssueTypeEnum do
+RSpec.describe Types::IssueTypeEnum, feature_category: :team_planning do
specify { expect(described_class.graphql_name).to eq('IssueType') }
it 'exposes all the existing issue type values except key_result' do
expect(described_class.values.keys).to match_array(
- %w[ISSUE INCIDENT TEST_CASE REQUIREMENT TASK OBJECTIVE]
+ %w[ISSUE INCIDENT TEST_CASE REQUIREMENT TASK OBJECTIVE KEY_RESULT]
)
end
end
diff --git a/spec/graphql/types/key_type_spec.rb b/spec/graphql/types/key_type_spec.rb
new file mode 100644
index 00000000000..78144076467
--- /dev/null
+++ b/spec/graphql/types/key_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['Key'], feature_category: :authentication_and_authorization do
+ specify { expect(described_class.graphql_name).to eq('Key') }
+
+ it 'contains attributes for SSH keys' do
+ expect(described_class).to have_graphql_fields(
+ :id, :title, :created_at, :expires_at, :key
+ )
+ end
+end
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index 7ab254238fb..8a4c89fc340 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -116,12 +116,12 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
describe 'merge_status_enum' do
let(:type) { GitlabSchema.types['MergeStatus'] }
+ let_it_be(:project) { create(:project, :public) }
+
it 'has the type MergeStatus' do
expect(described_class.fields['mergeStatusEnum']).to have_graphql_type(type)
end
- let_it_be(:project) { create(:project, :public) }
-
%i[preparing unchecked cannot_be_merged_recheck checking cannot_be_merged_rechecking can_be_merged cannot_be_merged].each do |state|
context "when the the DB value is #{state}" do
let(:merge_request) { create(:merge_request, :unique_branches, source_project: project, merge_status: state.to_s) }
diff --git a/spec/graphql/types/permission_types/base_permission_type_spec.rb b/spec/graphql/types/permission_types/base_permission_type_spec.rb
index e4726ad0e6e..f437c3778c6 100644
--- a/spec/graphql/types/permission_types/base_permission_type_spec.rb
+++ b/spec/graphql/types/permission_types/base_permission_type_spec.rb
@@ -51,4 +51,25 @@ RSpec.describe Types::PermissionTypes::BasePermissionType do
expect(test_type).to have_graphql_field(:admin_issue)
end
end
+
+ describe 'extensions' do
+ subject(:test_type) do
+ Class.new(described_class) do
+ graphql_name 'TestClass'
+
+ permission_field :read_entity_a do
+ extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
+ end
+
+ ability_field(:read_entity_b) do
+ extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
+ end
+ end
+ end
+
+ it 'has the extension' do
+ expect(test_type.fields['readEntityA'].extensions).to include(a_kind_of(::Gitlab::Graphql::Limit::FieldCallCount))
+ expect(test_type.fields['readEntityB'].extensions).to include(a_kind_of(::Gitlab::Graphql::Limit::FieldCallCount))
+ end
+ end
end
diff --git a/spec/graphql/types/permission_types/deployment_spec.rb b/spec/graphql/types/permission_types/deployment_spec.rb
new file mode 100644
index 00000000000..ccf5798984c
--- /dev/null
+++ b/spec/graphql/types/permission_types/deployment_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::PermissionTypes::Deployment, feature_category: :continuous_delivery do
+ it do
+ expected_permissions = %i[update_deployment destroy_deployment]
+
+ expect(described_class).to include_graphql_fields(*expected_permissions)
+ end
+end
diff --git a/spec/graphql/types/permission_types/environment_spec.rb b/spec/graphql/types/permission_types/environment_spec.rb
new file mode 100644
index 00000000000..944699c972a
--- /dev/null
+++ b/spec/graphql/types/permission_types/environment_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::PermissionTypes::Environment, feature_category: :continuous_delivery do
+ it do
+ expected_permissions = [
+ :update_environment, :destroy_environment, :stop_environment
+ ]
+
+ expected_permissions.each do |permission|
+ expect(described_class).to have_graphql_field(permission)
+ end
+ end
+end
diff --git a/spec/graphql/types/permission_types/project_spec.rb b/spec/graphql/types/permission_types/project_spec.rb
index c6853a0eadc..645fc6c68d1 100644
--- a/spec/graphql/types/permission_types/project_spec.rb
+++ b/spec/graphql/types/permission_types/project_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Types::PermissionTypes::Project do
:create_merge_request_from, :create_wiki, :push_code, :create_deployment, :push_to_delete_protected_branch,
:admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label,
:update_wiki, :destroy_wiki, :create_pages, :destroy_pages, :read_pages_content,
- :read_merge_request, :read_design, :create_design, :destroy_design
+ :read_merge_request, :read_design, :create_design, :destroy_design, :read_environment
]
expected_permissions.each do |permission|
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 30fabb8e9e2..4151789372b 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -492,7 +492,7 @@ RSpec.describe GitlabSchema.types['Project'] do
subject { described_class.fields['jobs'] }
it { is_expected.to have_graphql_type(Types::Ci::JobType.connection_type) }
- it { is_expected.to have_graphql_arguments(:statuses) }
+ it { is_expected.to have_graphql_arguments(:statuses, :with_artifacts) }
end
describe 'ci_template field' do
diff --git a/spec/graphql/types/projects/fork_details_type_spec.rb b/spec/graphql/types/projects/fork_details_type_spec.rb
new file mode 100644
index 00000000000..8e20e2c8299
--- /dev/null
+++ b/spec/graphql/types/projects/fork_details_type_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['ForkDetails'], feature_category: :source_code_management do
+ specify { expect(described_class.graphql_name).to eq('ForkDetails') }
+
+ it 'has specific fields' do
+ fields = %i[
+ ahead
+ behind
+ ]
+
+ expect(described_class).to have_graphql_fields(*fields)
+ end
+end
diff --git a/spec/graphql/types/projects/service_type_enum_spec.rb b/spec/graphql/types/projects/service_type_enum_spec.rb
index f7256910bb0..8b444a08c3b 100644
--- a/spec/graphql/types/projects/service_type_enum_spec.rb
+++ b/spec/graphql/types/projects/service_type_enum_spec.rb
@@ -23,7 +23,6 @@ RSpec.describe GitlabSchema.types['ServiceType'] do
EMAILS_ON_PUSH_SERVICE
EWM_SERVICE
EXTERNAL_WIKI_SERVICE
- FLOWDOCK_SERVICE
HANGOUTS_CHAT_SERVICE
IRKER_SERVICE
JENKINS_SERVICE
diff --git a/spec/graphql/types/snippets/blob_type_spec.rb b/spec/graphql/types/snippets/blob_type_spec.rb
index e20b001ba7f..9eef6ac4cdc 100644
--- a/spec/graphql/types/snippets/blob_type_spec.rb
+++ b/spec/graphql/types/snippets/blob_type_spec.rb
@@ -5,15 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['SnippetBlob'] do
include GraphqlHelpers
- it 'has the correct fields' do
- expected_fields = [:rich_data, :plain_data, :raw_plain_data,
- :raw_path, :size, :binary, :name, :path,
- :simple_viewer, :rich_viewer, :mode, :external_storage,
- :rendered_as_text]
-
- expect(described_class).to have_graphql_fields(*expected_fields)
- end
-
+ let_it_be(:blob) { create(:snippet, :public, :repository).blobs.first }
let_it_be(:nullity) do
{
'richData' => be_nullable,
@@ -32,7 +24,14 @@ RSpec.describe GitlabSchema.types['SnippetBlob'] do
}
end
- let_it_be(:blob) { create(:snippet, :public, :repository).blobs.first }
+ it 'has the correct fields' do
+ expected_fields = [:rich_data, :plain_data, :raw_plain_data,
+ :raw_path, :size, :binary, :name, :path,
+ :simple_viewer, :rich_viewer, :mode, :external_storage,
+ :rendered_as_text]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
shared_examples 'a field from the snippet blob presenter' do |field|
it "resolves using the presenter", :request_store do
diff --git a/spec/graphql/types/subscription_type_spec.rb b/spec/graphql/types/subscription_type_spec.rb
index 04f0c72b06f..a57a8e751ac 100644
--- a/spec/graphql/types/subscription_type_spec.rb
+++ b/spec/graphql/types/subscription_type_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe GitlabSchema.types['Subscription'] do
issuable_milestone_updated
merge_request_reviewers_updated
merge_request_merge_status_updated
+ merge_request_approval_state_updated
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/todo_type_spec.rb b/spec/graphql/types/todo_type_spec.rb
index c7bb7c67959..2118a777a45 100644
--- a/spec/graphql/types/todo_type_spec.rb
+++ b/spec/graphql/types/todo_type_spec.rb
@@ -3,6 +3,11 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['Todo'] do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:author) { create(:user) }
+
+ let(:issue) { create(:issue, project: project) }
+
it 'has the correct fields' do
expected_fields = [
:id,
@@ -22,4 +27,116 @@ RSpec.describe GitlabSchema.types['Todo'] do
end
specify { expect(described_class).to require_graphql_authorizations(:read_todo) }
+
+ subject { GitlabSchema.execute(query, context: { current_user: current_user }).as_json }
+
+ describe 'project field' do
+ let(:todo) do
+ create(:todo,
+ user: current_user,
+ project: project,
+ state: :done,
+ action: Todo::ASSIGNED,
+ author: author,
+ target: issue)
+ end
+
+ let(:query) do
+ %(
+ query {
+ todo(id: "#{todo.to_global_id}") {
+ project {
+ id
+ }
+ }
+ }
+ )
+ end
+
+ context 'when the project is public' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'when the user does not have access' do
+ it 'returns the project' do
+ expect(subject.dig('data', 'todo', 'project', 'id')).to eq(project.to_global_id.to_s)
+ end
+ end
+ end
+
+ context 'when the project is not public' do
+ let_it_be(:project) { create(:project) }
+
+ context 'when the user does not have access' do
+ it 'returns null' do
+ expect(subject.dig('data', 'todo', 'project')).to be_nil
+ end
+ end
+
+ context 'when the user does have access' do
+ before do
+ project.add_guest(current_user)
+ end
+
+ it 'returns the project' do
+ expect(subject.dig('data', 'todo', 'project', 'id')).to eq(project.to_global_id.to_s)
+ end
+ end
+ end
+ end
+
+ describe 'group field' do
+ let(:todo) do
+ create(:todo,
+ user: current_user,
+ group: group,
+ state: :done,
+ action: Todo::MENTIONED,
+ author: author,
+ target: issue)
+ end
+
+ let(:query) do
+ %(
+ query {
+ todo(id: "#{todo.to_global_id}") {
+ group {
+ id
+ }
+ }
+ }
+ )
+ end
+
+ context 'when the group is public' do
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+
+ context 'when the user does not have access' do
+ it 'returns the group' do
+ expect(subject.dig('data', 'todo', 'group', 'id')).to eq(group.to_global_id.to_s)
+ end
+ end
+ end
+
+ context 'when the group is not public' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ context 'when the user does not have access' do
+ it 'returns null' do
+ expect(subject.dig('data', 'todo', 'group')).to be_nil
+ end
+ end
+
+ context 'when the user does have access' do
+ before do
+ group.add_guest(current_user)
+ end
+
+ it 'returns the group' do
+ expect(subject.dig('data', 'todo', 'group', 'id')).to eq(group.to_global_id.to_s)
+ end
+ end
+ end
+ end
end
diff --git a/spec/graphql/types/work_items/notes_filter_type_enum_spec.rb b/spec/graphql/types/work_items/notes_filter_type_enum_spec.rb
new file mode 100644
index 00000000000..13ce559c529
--- /dev/null
+++ b/spec/graphql/types/work_items/notes_filter_type_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['NotesFilterType'], feature_category: :team_planning do
+ specify { expect(described_class.graphql_name).to eq('NotesFilterType') }
+
+ it 'exposes all the existing widget type values' do
+ expect(described_class.values.transform_values(&:value)).to include(
+ "ALL_NOTES" => 0, "ONLY_ACTIVITY" => 2, "ONLY_COMMENTS" => 1
+ )
+ end
+end
diff --git a/spec/graphql/types/work_items/widget_interface_spec.rb b/spec/graphql/types/work_items/widget_interface_spec.rb
index b9e8edacf15..a2b12ed52dc 100644
--- a/spec/graphql/types/work_items/widget_interface_spec.rb
+++ b/spec/graphql/types/work_items/widget_interface_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe Types::WorkItems::WidgetInterface do
WorkItems::Widgets::Hierarchy | Types::WorkItems::Widgets::HierarchyType
WorkItems::Widgets::Assignees | Types::WorkItems::Widgets::AssigneesType
WorkItems::Widgets::Labels | Types::WorkItems::Widgets::LabelsType
+ WorkItems::Widgets::Notes | Types::WorkItems::Widgets::NotesType
end
with_them do
diff --git a/spec/graphql/types/work_items/widgets/hierarchy_type_spec.rb b/spec/graphql/types/work_items/widgets/hierarchy_type_spec.rb
index 1722a07c5f4..20413a35c58 100644
--- a/spec/graphql/types/work_items/widgets/hierarchy_type_spec.rb
+++ b/spec/graphql/types/work_items/widgets/hierarchy_type_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
-RSpec.describe Types::WorkItems::Widgets::HierarchyType do
+RSpec.describe Types::WorkItems::Widgets::HierarchyType, feature_category: :team_planning do
it 'exposes the expected fields' do
- expected_fields = %i[parent children type]
+ expected_fields = %i[parent children has_children type]
expect(described_class).to have_graphql_fields(*expected_fields)
end
diff --git a/spec/graphql/types/work_items/widgets/notes_type_spec.rb b/spec/graphql/types/work_items/widgets/notes_type_spec.rb
new file mode 100644
index 00000000000..3ac61a59a9c
--- /dev/null
+++ b/spec/graphql/types/work_items/widgets/notes_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::WorkItems::Widgets::NotesType, feature_category: :team_planning do
+ it 'exposes the expected fields' do
+ expected_fields = %i[discussions type]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/haml_lint/linter/documentation_links_spec.rb b/spec/haml_lint/linter/documentation_links_spec.rb
index 49a720700da..380df49cde3 100644
--- a/spec/haml_lint/linter/documentation_links_spec.rb
+++ b/spec/haml_lint/linter/documentation_links_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe HamlLint::Linter::DocumentationLinks do
end
context 'when link_to points to the existing file with valid anchor' do
- let(:haml) { "= link_to 'Description', #{link_pattern}('index.md', anchor: 'overview'), target: '_blank'" }
+ let(:haml) { "= link_to 'Description', #{link_pattern}('index.md', anchor: 'user-account'), target: '_blank'" }
it { is_expected.not_to report_lint }
end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 261d8c8c302..3384f9fea05 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe ApplicationHelper do
+ include Devise::Test::ControllerHelpers
+
describe 'current_controller?' do
before do
stub_controller_name('foo')
@@ -419,7 +421,7 @@ RSpec.describe ApplicationHelper do
end
it 'includes all possible body data elements and associates the project elements with project' do
- expect(helper).to receive(:can?).with(nil, :download_code, project)
+ expect(helper).to receive(:can?).with(nil, :read_code, project)
expect(helper.body_data).to eq(
{
page: 'application',
@@ -437,7 +439,7 @@ RSpec.describe ApplicationHelper do
let_it_be(:project) { create(:project, :repository, group: create(:group)) }
it 'includes all possible body data elements and associates the project elements with project' do
- expect(helper).to receive(:can?).with(nil, :download_code, project)
+ expect(helper).to receive(:can?).with(nil, :read_code, project)
expect(helper.body_data).to eq(
{
page: 'application',
@@ -463,7 +465,7 @@ RSpec.describe ApplicationHelper do
stub_controller_method(:action_name, 'show')
stub_controller_method(:params, { id: issue.id })
- expect(helper).to receive(:can?).with(nil, :download_code, project).and_return(false)
+ expect(helper).to receive(:can?).with(nil, :read_code, project).and_return(false)
expect(helper.body_data).to eq(
{
page: 'projects:issues:show',
@@ -479,12 +481,34 @@ RSpec.describe ApplicationHelper do
end
end
- context 'when current_user has download_code permission' do
- it 'returns find_file with the default branch' do
+ describe 'find_file attribute' do
+ subject { helper.body_data[:find_file] }
+
+ before do
allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ context 'when the project has no repository' do
+ before do
+ allow(project).to receive(:empty_repo?).and_return(true)
+ end
+
+ it { is_expected.to be_nil }
+ end
- expect(helper).to receive(:can?).with(user, :download_code, project).and_return(true)
- expect(helper.body_data[:find_file]).to end_with(project.default_branch)
+ context 'when user cannot read_code for the project' do
+ before do
+ allow(helper).to receive(:can?).with(user, :read_code, project).and_return(false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when current_user has read_code permission' do
+ it 'returns find_file with the default branch' do
+ expect(helper).to receive(:can?).with(user, :read_code, project).and_return(true)
+ expect(subject).to end_with(project.default_branch)
+ end
end
end
end
@@ -651,4 +675,40 @@ RSpec.describe ApplicationHelper do
end
end
end
+
+ describe 'stylesheet_link_tag_defer' do
+ it 'uses print stylesheet by default' do
+ expect(helper.stylesheet_link_tag_defer('test')).to eq( '<link rel="stylesheet" media="print" href="/stylesheets/test.css" />')
+ end
+
+ it 'uses regular stylesheet when no_startup_css param present' do
+ allow(helper.controller).to receive(:params).and_return({ no_startup_css: '' })
+
+ expect(helper.stylesheet_link_tag_defer('test')).to eq( '<link rel="stylesheet" media="screen" href="/stylesheets/test.css" />')
+ end
+ end
+
+ describe '#use_new_fonts?' do
+ subject { helper.use_new_fonts? }
+
+ it { is_expected.to eq true }
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(new_fonts: false)
+ end
+
+ it { is_expected.to eq false }
+
+ context 'with special request param' do
+ let(:request) { instance_double(ActionController::TestRequest, params: { new_fonts: true }) }
+
+ before do
+ allow(helper).to receive(:request).and_return(request)
+ end
+
+ it { is_expected.to eq true }
+ end
+ end
+ end
end
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index eafdbfa8d0a..914c866c464 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -273,15 +273,9 @@ RSpec.describe ApplicationSettingsHelper do
end
end
- describe '.registration_features_can_be_prompted?' do
+ describe '.registration_features_can_be_prompted?', :without_license do
subject { helper.registration_features_can_be_prompted? }
- before do
- if Gitlab.ee?
- allow(License).to receive(:current).and_return(nil)
- end
- end
-
context 'when service ping is enabled' do
before do
stub_application_setting(usage_ping_enabled: true)
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
index 9c0f8b77d45..cef72d24c43 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -297,28 +297,22 @@ RSpec.describe AvatarsHelper do
subject { helper.user_avatar_without_link(options) }
it 'displays user avatar' do
- is_expected.to eq tag(
- :img,
- alt: "#{user.name}'s avatar",
- src: avatar_icon_for_user(user, 16),
- data: { container: 'body' },
- class: 'avatar s16 has-tooltip',
- title: user.name
- )
+ is_expected.to eq tag.img(alt: "#{user.name}'s avatar",
+ src: avatar_icon_for_user(user, 16),
+ data: { container: 'body' },
+ class: 'avatar s16 has-tooltip',
+ title: user.name)
end
context 'with css_class parameter' do
let(:options) { { user: user, css_class: '.cat-pics' } }
it 'uses provided css_class' do
- is_expected.to eq tag(
- :img,
- alt: "#{user.name}'s avatar",
- src: avatar_icon_for_user(user, 16),
- data: { container: 'body' },
- class: "avatar s16 #{options[:css_class]} has-tooltip",
- title: user.name
- )
+ is_expected.to eq tag.img(alt: "#{user.name}'s avatar",
+ src: avatar_icon_for_user(user, 16),
+ data: { container: 'body' },
+ class: "avatar s16 #{options[:css_class]} has-tooltip",
+ title: user.name)
end
end
@@ -326,14 +320,11 @@ RSpec.describe AvatarsHelper do
let(:options) { { user: user, size: 99 } }
it 'uses provided size' do
- is_expected.to eq tag(
- :img,
- alt: "#{user.name}'s avatar",
- src: avatar_icon_for_user(user, options[:size]),
- data: { container: 'body' },
- class: "avatar s#{options[:size]} has-tooltip",
- title: user.name
- )
+ is_expected.to eq tag.img(alt: "#{user.name}'s avatar",
+ src: avatar_icon_for_user(user, options[:size]),
+ data: { container: 'body' },
+ class: "avatar s#{options[:size]} has-tooltip",
+ title: user.name)
end
end
@@ -341,14 +332,11 @@ RSpec.describe AvatarsHelper do
let(:options) { { user: user, url: '/over/the/rainbow.png' } }
it 'uses provided url' do
- is_expected.to eq tag(
- :img,
- alt: "#{user.name}'s avatar",
- src: options[:url],
- data: { container: 'body' },
- class: "avatar s16 has-tooltip",
- title: user.name
- )
+ is_expected.to eq tag.img(alt: "#{user.name}'s avatar",
+ src: options[:url],
+ data: { container: 'body' },
+ class: "avatar s16 has-tooltip",
+ title: user.name)
end
end
@@ -356,14 +344,11 @@ RSpec.describe AvatarsHelper do
let(:options) { { user: user, lazy: true } }
it 'adds `lazy` class to class list, sets `data-src` with avatar URL and `src` with placeholder image' do
- is_expected.to eq tag(
- :img,
- alt: "#{user.name}'s avatar",
- src: LazyImageTagHelper.placeholder_image,
- data: { container: 'body', src: avatar_icon_for_user(user, 16) },
- class: "avatar s16 has-tooltip lazy",
- title: user.name
- )
+ is_expected.to eq tag.img(alt: "#{user.name}'s avatar",
+ src: LazyImageTagHelper.placeholder_image,
+ data: { container: 'body', src: avatar_icon_for_user(user, 16) },
+ class: "avatar s16 has-tooltip lazy",
+ title: user.name)
end
end
@@ -372,14 +357,11 @@ RSpec.describe AvatarsHelper do
let(:options) { { user: user, has_tooltip: true } }
it 'adds has-tooltip' do
- is_expected.to eq tag(
- :img,
- alt: "#{user.name}'s avatar",
- src: avatar_icon_for_user(user, 16),
- data: { container: 'body' },
- class: "avatar s16 has-tooltip",
- title: user.name
- )
+ is_expected.to eq tag.img(alt: "#{user.name}'s avatar",
+ src: avatar_icon_for_user(user, 16),
+ data: { container: 'body' },
+ class: "avatar s16 has-tooltip",
+ title: user.name)
end
end
@@ -387,13 +369,10 @@ RSpec.describe AvatarsHelper do
let(:options) { { user: user, has_tooltip: false } }
it 'does not add has-tooltip or data container' do
- is_expected.to eq tag(
- :img,
- alt: "#{user.name}'s avatar",
- src: avatar_icon_for_user(user, 16),
- class: "avatar s16",
- title: user.name
- )
+ is_expected.to eq tag.img(alt: "#{user.name}'s avatar",
+ src: avatar_icon_for_user(user, 16),
+ class: "avatar s16",
+ title: user.name)
end
end
end
@@ -405,26 +384,20 @@ RSpec.describe AvatarsHelper do
let(:options) { { user: user, user_name: 'Tinky Winky' } }
it 'prefers user parameter' do
- is_expected.to eq tag(
- :img,
- alt: "#{user.name}'s avatar",
- src: avatar_icon_for_user(user, 16),
- data: { container: 'body' },
- class: "avatar s16 has-tooltip",
- title: user.name
- )
+ is_expected.to eq tag.img(alt: "#{user.name}'s avatar",
+ src: avatar_icon_for_user(user, 16),
+ data: { container: 'body' },
+ class: "avatar s16 has-tooltip",
+ title: user.name)
end
end
it 'uses user_name and user_email parameter if user is not present' do
- is_expected.to eq tag(
- :img,
- alt: "#{options[:user_name]}'s avatar",
- src: helper.avatar_icon_for_email(options[:user_email], 16),
- data: { container: 'body' },
- class: "avatar s16 has-tooltip",
- title: options[:user_name]
- )
+ is_expected.to eq tag.img(alt: "#{options[:user_name]}'s avatar",
+ src: helper.avatar_icon_for_email(options[:user_email], 16),
+ data: { container: 'body' },
+ class: "avatar s16 has-tooltip",
+ title: options[:user_name])
end
end
@@ -435,14 +408,11 @@ RSpec.describe AvatarsHelper do
let(:options) { { user: user_with_avatar, only_path: false } }
it 'will return avatar with a full path' do
- is_expected.to eq tag(
- :img,
- alt: "#{user_with_avatar.name}'s avatar",
- src: avatar_icon_for_user(user_with_avatar, 16, only_path: false),
- data: { container: 'body' },
- class: "avatar s16 has-tooltip",
- title: user_with_avatar.name
- )
+ is_expected.to eq tag.img(alt: "#{user_with_avatar.name}'s avatar",
+ src: avatar_icon_for_user(user_with_avatar, 16, only_path: false),
+ data: { container: 'body' },
+ class: "avatar s16 has-tooltip",
+ title: user_with_avatar.name)
end
end
@@ -450,14 +420,11 @@ RSpec.describe AvatarsHelper do
let(:options) { { user_email: user_with_avatar.email, user_name: user_with_avatar.username, only_path: false } }
it 'will return avatar with a full path' do
- is_expected.to eq tag(
- :img,
- alt: "#{user_with_avatar.username}'s avatar",
- src: helper.avatar_icon_for_email(user_with_avatar.email, 16, only_path: false),
- data: { container: 'body' },
- class: "avatar s16 has-tooltip",
- title: user_with_avatar.username
- )
+ is_expected.to eq tag.img(alt: "#{user_with_avatar.username}'s avatar",
+ src: helper.avatar_icon_for_email(user_with_avatar.email, 16, only_path: false),
+ data: { container: 'body' },
+ class: "avatar s16 has-tooltip",
+ title: user_with_avatar.username)
end
end
end
@@ -480,14 +447,11 @@ RSpec.describe AvatarsHelper do
let(:resource) { user.namespace }
it 'displays user avatar' do
- is_expected.to eq tag(
- :img,
- alt: "#{user.name}'s avatar",
- src: avatar_icon_for_user(user, 32),
- data: { container: 'body' },
- class: 'avatar s32 has-tooltip',
- title: user.name
- )
+ is_expected.to eq tag.img(alt: "#{user.name}'s avatar",
+ src: avatar_icon_for_user(user, 32),
+ data: { container: 'body' },
+ class: 'avatar s32 has-tooltip',
+ title: user.name)
end
end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index fe652e905cc..dac0d3fe182 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -80,6 +80,7 @@ RSpec.describe BlobHelper do
end
end
end
+
context 'viewer related' do
include FakeBlobHelpers
diff --git a/spec/helpers/ci/secure_files_helper_spec.rb b/spec/helpers/ci/secure_files_helper_spec.rb
index 02da44f56b2..54307e670e1 100644
--- a/spec/helpers/ci/secure_files_helper_spec.rb
+++ b/spec/helpers/ci/secure_files_helper_spec.rb
@@ -19,58 +19,40 @@ RSpec.describe Ci::SecureFilesHelper do
subject { helper.show_secure_files_setting(project, user) }
describe '#show_secure_files_setting' do
- context 'when the :ci_secure_files feature flag is enabled' do
- before do
- stub_feature_flags(ci_secure_files: true)
- end
+ context 'authenticated user with admin permissions' do
+ let(:user) { maintainer }
- context 'authenticated user with admin permissions' do
- let(:user) { maintainer }
-
- it { is_expected.to be true }
- end
-
- context 'authenticated user with read permissions' do
- let(:user) { developer }
-
- it { is_expected.to be true }
- end
+ it { is_expected.to be true }
+ end
- context 'authenticated user with guest permissions' do
- let(:user) { guest }
+ context 'authenticated user with read permissions' do
+ let(:user) { developer }
- it { is_expected.to be false }
- end
+ it { is_expected.to be true }
+ end
- context 'authenticated user with no permissions' do
- let(:user) { anonymous }
+ context 'authenticated user with guest permissions' do
+ let(:user) { guest }
- it { is_expected.to be false }
- end
+ it { is_expected.to be false }
+ end
- context 'unconfirmed user' do
- let(:user) { unconfirmed }
+ context 'authenticated user with no permissions' do
+ let(:user) { anonymous }
- it { is_expected.to be false }
- end
+ it { is_expected.to be false }
+ end
- context 'unauthenticated user' do
- let(:user) { nil }
+ context 'unconfirmed user' do
+ let(:user) { unconfirmed }
- it { is_expected.to be false }
- end
+ it { is_expected.to be false }
end
- context 'when the :ci_secure_files feature flag is disabled' do
- before do
- stub_feature_flags(ci_secure_files: false)
- end
-
- context 'authenticated user with admin permissions' do
- let(:user) { maintainer }
+ context 'unauthenticated user' do
+ let(:user) { nil }
- it { is_expected.to be false }
- end
+ it { is_expected.to be false }
end
end
end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 78c0d0a2b11..a46f8c13f00 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -483,7 +483,18 @@ RSpec.describe DiffHelper do
end
describe '#conflicts' do
- let(:merge_request) { instance_double(MergeRequest, cannot_be_merged?: true) }
+ let(:merge_request) do
+ instance_double(
+ MergeRequest,
+ cannot_be_merged?: cannot_be_merged?,
+ source_branch_exists?: source_branch_exists?,
+ target_branch_exists?: target_branch_exists?
+ )
+ end
+
+ let(:cannot_be_merged?) { true }
+ let(:source_branch_exists?) { true }
+ let(:target_branch_exists?) { true }
let(:can_be_resolved_in_ui?) { true }
let(:allow_tree_conflicts) { false }
let(:files) { [instance_double(Gitlab::Conflict::File, path: 'a')] }
@@ -508,7 +519,23 @@ RSpec.describe DiffHelper do
end
context 'when merge request can be merged' do
- let(:merge_request) { instance_double(MergeRequest, cannot_be_merged?: false) }
+ let(:cannot_be_merged?) { false }
+
+ it 'returns nil' do
+ expect(helper.conflicts).to be_nil
+ end
+ end
+
+ context 'when source branch does not exist' do
+ let(:source_branch_exists?) { false }
+
+ it 'returns nil' do
+ expect(helper.conflicts).to be_nil
+ end
+ end
+
+ context 'when target branch does not exist' do
+ let(:target_branch_exists?) { false }
it 'returns nil' do
expect(helper.conflicts).to be_nil
diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb
index 1fcbcd8c4f9..c8d67d6dac2 100644
--- a/spec/helpers/environment_helper_spec.rb
+++ b/spec/helpers/environment_helper_spec.rb
@@ -50,6 +50,7 @@ RSpec.describe EnvironmentHelper do
expect(subject).to eq({
name: environment.name,
id: environment.id,
+ project_full_path: project.full_path,
external_url: environment.external_url,
can_update_environment: true,
can_destroy_environment: true,
diff --git a/spec/helpers/git_helper_spec.rb b/spec/helpers/git_helper_spec.rb
index 0dd9eecb7f0..543b9ce7a82 100644
--- a/spec/helpers/git_helper_spec.rb
+++ b/spec/helpers/git_helper_spec.rb
@@ -15,11 +15,13 @@ RSpec.describe GitHelper do
it { expect(strip_signature).to eq("Version 1.69.0\n\n") }
end
+
context 'strips PGP MESSAGE' do
let(:strip_signature) { helper.strip_signature( pgp_message_tag ) }
it { expect(strip_signature).to eq("Version 1.69.0\n\n") }
end
+
context 'strips SIGNED MESSAGE' do
let(:strip_signature) { helper.strip_signature( x509_message_tag ) }
diff --git a/spec/helpers/groups/observability_helper_spec.rb b/spec/helpers/groups/observability_helper_spec.rb
index 4393f4e9bec..6d0a8631f78 100644
--- a/spec/helpers/groups/observability_helper_spec.rb
+++ b/spec/helpers/groups/observability_helper_spec.rb
@@ -10,17 +10,17 @@ RSpec.describe Groups::ObservabilityHelper do
context 'if observability_path is missing from params' do
it 'returns the iframe src for action: dashboards' do
allow(helper).to receive(:params).and_return({ action: 'dashboards' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/")
+ expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/")
end
it 'returns the iframe src for action: manage' do
allow(helper).to receive(:params).and_return({ action: 'manage' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/dashboards")
+ expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/dashboards")
end
it 'returns the iframe src for action: explore' do
allow(helper).to receive(:params).and_return({ action: 'explore' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/explore")
+ expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/explore")
end
end
@@ -28,7 +28,7 @@ RSpec.describe Groups::ObservabilityHelper do
context 'if observability_path is valid' do
it 'returns the iframe src by injecting the observability path' do
allow(helper).to receive(:params).and_return({ action: '/explore', observability_path: '/foo?bar=foobar' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/foo?bar=foobar")
+ expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/foo?bar=foobar")
end
end
@@ -40,7 +40,7 @@ RSpec.describe Groups::ObservabilityHelper do
"/test?groupId=<script>alert('attack!')</script>"
})
expect(helper.observability_iframe_src(group)).to eq(
- "#{observability_url}/#{group.id}/test?groupId=alert('attack!')"
+ "#{observability_url}/-/#{group.id}/test?groupId=alert('attack!')"
)
end
end
diff --git a/spec/helpers/groups/settings_helper_spec.rb b/spec/helpers/groups/settings_helper_spec.rb
index f8c0bfc19a1..ed948f5456c 100644
--- a/spec/helpers/groups/settings_helper_spec.rb
+++ b/spec/helpers/groups/settings_helper_spec.rb
@@ -11,8 +11,7 @@ RSpec.describe Groups::SettingsHelper do
using RSpec::Parameterized::TableSyntax
fake_form_id = "fake_form_id"
-
- where(:is_paid, :is_button_disabled, :form_value_id) do
+ where(:prevent_delete_response, :is_button_disabled, :form_value_id) do
true | "true" | nil
true | "true" | fake_form_id
false | "false" | nil
@@ -21,7 +20,7 @@ RSpec.describe Groups::SettingsHelper do
with_them do
it "returns expected parameters" do
- allow(group).to receive(:paid?).and_return(is_paid)
+ allow(group).to receive(:prevent_delete?).and_return(prevent_delete_response)
expected = helper.group_settings_confirm_modal_data(group, form_value_id)
expect(expected).to eq({
diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb
index 447967fd345..29b2784412e 100644
--- a/spec/helpers/ide_helper_spec.rb
+++ b/spec/helpers/ide_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IdeHelper do
+RSpec.describe IdeHelper, feature_category: :web_ide do
describe '#ide_data' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { project.creator }
@@ -18,6 +18,8 @@ RSpec.describe IdeHelper do
self.instance_variable_set(:@branch, 'master')
self.instance_variable_set(:@project, project)
+ self.instance_variable_set(:@path, 'foo/README.md')
+ self.instance_variable_set(:@merge_request, '7')
end
it 'returns hash' do
@@ -30,7 +32,11 @@ RSpec.describe IdeHelper do
help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
'branch-name' => 'master',
'project-path' => project.path_with_namespace,
- 'csp-nonce' => 'test-csp-nonce'
+ 'csp-nonce' => 'test-csp-nonce',
+ 'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path'),
+ 'file-path' => 'foo/README.md',
+ 'merge-request' => '7',
+ 'fork-info' => nil
)
end
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index c753d553371..48e94ec7e98 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -21,7 +21,8 @@ RSpec.describe InviteMembersHelper do
invalid_groups: project.related_group_ids,
help_link: help_page_url('user/permissions'),
is_project: 'true',
- access_levels: ProjectMember.access_level_roles.to_json
+ access_levels: ProjectMember.access_level_roles.to_json,
+ full_path: project.full_path
}
expect(helper.common_invite_group_modal_data(project, ProjectMember, 'true')).to include(attributes)
@@ -56,7 +57,8 @@ RSpec.describe InviteMembersHelper do
id: project.id,
root_id: project.root_ancestor.id,
name: project.name,
- default_access_level: Gitlab::Access::GUEST
+ default_access_level: Gitlab::Access::GUEST,
+ full_path: project.full_path
}
expect(helper.common_invite_modal_dataset(project)).to include(attributes)
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 18a21b59409..15b57a4c9eb 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -98,6 +98,66 @@ RSpec.describe IssuablesHelper do
end
end
+ describe '#assigned_issuables_count', feature_category: :project_management do
+ context 'when issuable is issues' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project).tap { |p| p.add_developer(user) } }
+
+ subject { helper.assigned_issuables_count(:issues) }
+
+ before 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]) }
+
+ before do
+ stub_feature_flags(limit_assigned_issues_count: false)
+ end
+
+ it { is_expected.to eq 101 }
+
+ context 'when FF limit_assigned_issues_count is enabled' do
+ before do
+ stub_feature_flags(limit_assigned_issues_count: true)
+ end
+
+ it { is_expected.to eq 100 }
+ end
+ end
+ end
+ end
+
+ describe '#assigned_open_issues_count_text', feature_category: :project_management do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project).tap { |p| p.add_developer(user) } }
+
+ subject { helper.assigned_open_issues_count_text }
+
+ before 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]) }
+
+ before do
+ stub_feature_flags(limit_assigned_issues_count: false)
+ end
+
+ it { is_expected.to eq '100' }
+
+ context 'when FF limit_assigned_issues_count is enabled' do
+ before do
+ stub_feature_flags(limit_assigned_issues_count: true)
+ end
+
+ it { is_expected.to eq '99+' }
+ end
+ end
+ end
+
describe '#issuable_meta', time_travel_to: '2022-08-05 00:00:00 +0000' do
let(:user) { create(:user) }
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index e5bd8e6532f..ed363268cdf 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -3,9 +3,8 @@
require 'spec_helper'
RSpec.describe IssuesHelper do
- let(:project) { create(:project) }
- let(:issue) { create(:issue, project: project) }
- let(:ext_project) { create(:project, :with_redmine_integration) }
+ let_it_be(:project) { create(:project) }
+ let_it_be_with_reload(:issue) { create(:issue, project: project) }
describe '#work_item_type_icon' do
it 'returns icon of all standard base types' do
@@ -381,6 +380,27 @@ RSpec.describe IssuesHelper do
end
end
+ describe '#dashboard_issues_list_data' do
+ let(:current_user) { double.as_null_object }
+
+ it 'returns expected result' do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ allow(helper).to receive(:image_path).and_return('#')
+ allow(helper).to receive(:url_for).and_return('#')
+
+ expected = {
+ calendar_path: '#',
+ empty_state_svg_path: '#',
+ initial_sort: current_user&.user_preference&.issues_sort,
+ is_public_visibility_restricted: Gitlab::CurrentSettings.restricted_visibility_levels ? 'false' : '',
+ is_signed_in: current_user.present?.to_s,
+ rss_path: '#'
+ }
+
+ expect(helper.dashboard_issues_list_data(current_user)).to include(expected)
+ end
+ end
+
describe '#issues_form_data' do
it 'returns expected result' do
expected = {
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 85420d4afda..e8e981251e3 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -321,4 +321,27 @@ RSpec.describe LabelsHelper do
expect(wrap_label_html('xss', label: xss_label, small: false)).not_to include('color:')
end
end
+
+ describe '#label_subscription_toggle_button_text' do
+ let(:label) { instance_double(Label) }
+ let(:current_user) { instance_double(User) }
+
+ subject { label_subscription_toggle_button_text(label) }
+
+ context 'when the label is subscribed' do
+ before do
+ allow(label).to receive(:subscribed?).and_return(true)
+ end
+
+ it { is_expected.to eq(_('Unsubscribe')) }
+ end
+
+ context 'when the label is not subscribed' do
+ before do
+ allow(label).to receive(:subscribed?).and_return(false)
+ end
+
+ it { is_expected.to eq(_('Subscribe')) }
+ end
+ end
end
diff --git a/spec/helpers/listbox_helper_spec.rb b/spec/helpers/listbox_helper_spec.rb
index cba00b43ae5..bae9c40aa02 100644
--- a/spec/helpers/listbox_helper_spec.rb
+++ b/spec/helpers/listbox_helper_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe ListboxHelper do
*%w[
dropdown
b-dropdown
- gl-new-dropdown
+ gl-dropdown
btn-group
js-redirect-listbox
])
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 0b3d400041c..d1c86abf6e9 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -445,11 +445,25 @@ RSpec.describe MarkupHelper do
shared_examples_for 'common markdown examples' do
let(:project_base) { build(:project, :repository) }
+ it 'displays inline code' do
+ object = create_object('Text with `inline code`')
+ expected = 'Text with <code>inline code</code>'
+
+ expect(first_line_in_markdown(object, attribute, 100, is_todo: true, project: project)).to match(expected)
+ end
+
+ it 'truncates the text with multiple paragraphs' do
+ object = create_object("Paragraph 1\n\nParagraph 2")
+ expected = 'Paragraph 1...'
+
+ expect(first_line_in_markdown(object, attribute, 100, is_todo: true, project: project)).to match(expected)
+ end
+
it 'displays the first line of a code block' do
object = create_object("```\nCode block\nwith two lines\n```")
expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
- expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
+ expect(first_line_in_markdown(object, attribute, 100, is_todo: true, project: project)).to match(expected)
end
it 'truncates a single long line of text' do
@@ -457,7 +471,7 @@ RSpec.describe MarkupHelper do
object = create_object(text * 4)
expected = (text * 2).sub(/.{3}/, '...')
- expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected)
+ expect(first_line_in_markdown(object, attribute, 150, is_todo: true, project: project)).to match(expected)
end
it 'preserves code color scheme' do
@@ -466,28 +480,15 @@ RSpec.describe MarkupHelper do
"<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
"</code></pre>\n"
- expect(first_line_in_markdown(object, attribute, 150, project: project)).to eq(expected)
+ expect(first_line_in_markdown(object, attribute, 150, is_todo: true, project: project)).to eq(expected)
end
- context 'when images are allowed' do
- it 'preserves data-src for lazy images' do
- object = create_object("![ImageTest](/uploads/test.png)")
- image_url = "data-src=\".*/uploads/test.png\""
- text = first_line_in_markdown(object, attribute, 150, project: project, allow_images: true)
+ it 'removes any images' do
+ object = create_object("![ImageTest](/uploads/test.png)")
+ text = first_line_in_markdown(object, attribute, 150, is_todo: true, project: project)
- expect(text).to match(image_url)
- expect(text).to match('<a')
- end
- end
-
- context 'when images are not allowed' do
- it 'removes any images' do
- object = create_object("![ImageTest](/uploads/test.png)")
- text = first_line_in_markdown(object, attribute, 150, project: project)
-
- expect(text).not_to match('<img')
- expect(text).not_to match('<a')
- end
+ expect(text).not_to match('<img')
+ expect(text).not_to match('<a')
end
context 'labels formatting' do
@@ -497,7 +498,7 @@ RSpec.describe MarkupHelper do
create(:label, title: 'label_1', project: project)
object = create_object(label_title, project: project)
- first_line_in_markdown(object, attribute, 150, project: project)
+ first_line_in_markdown(object, attribute, 150, is_todo: true, project: project)
end
it 'preserves style attribute for a label that can be accessed by current_user' do
@@ -518,10 +519,10 @@ RSpec.describe MarkupHelper do
end
it 'keeps whitelisted tags' do
- html = '<a><i></i></a> <strong>strong</strong><em>em</em><b>b</b>'
+ html = '<i></i> <strong>strong</strong><em>em</em><b>b</b>'
object = create_object(html)
- result = first_line_in_markdown(object, attribute, 100, project: project)
+ result = first_line_in_markdown(object, attribute, 100, is_todo: true, project: project)
expect(result).to include(html)
end
@@ -530,7 +531,7 @@ RSpec.describe MarkupHelper do
object = create_object("hello \n\n [Test](README.md)")
expect do
- first_line_in_markdown(object, attribute, nil, project: project)
+ first_line_in_markdown(object, attribute, 100, is_todo: true, project: project)
end.not_to change { Gitlab::GitalyClient.get_request_count }
end
end
diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb
index 0d43cfaae90..c4a8536032e 100644
--- a/spec/helpers/nav/top_nav_helper_spec.rb
+++ b/spec/helpers/nav/top_nav_helper_spec.rb
@@ -122,10 +122,10 @@ RSpec.describe Nav::TopNavHelper do
title: 'Switch to'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
- css_class: 'qa-projects-dropdown',
data: {
track_action: 'click_dropdown',
- track_label: 'projects_dropdown'
+ track_label: 'projects_dropdown',
+ qa_selector: 'projects_dropdown'
},
icon: 'project',
id: 'project',
@@ -219,10 +219,10 @@ RSpec.describe Nav::TopNavHelper do
title: 'Switch to'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
- css_class: 'qa-groups-dropdown',
data: {
track_action: 'click_dropdown',
- track_label: 'groups_dropdown'
+ track_label: 'groups_dropdown',
+ qa_selector: 'groups_dropdown'
},
icon: 'group',
id: 'groups',
@@ -323,10 +323,7 @@ RSpec.describe Nav::TopNavHelper do
title: 'Explore'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'milestones_link',
- **menu_data_tracking_attrs('milestones')
- },
+ data: { **menu_data_tracking_attrs('milestones') },
href: '/dashboard/milestones',
icon: 'clock',
id: 'milestones',
@@ -385,10 +382,7 @@ RSpec.describe Nav::TopNavHelper do
title: 'Explore'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'activity_link',
- **menu_data_tracking_attrs('activity')
- },
+ data: { **menu_data_tracking_attrs('activity') },
href: '/dashboard/activity',
icon: 'history',
id: 'activity',
@@ -417,15 +411,13 @@ RSpec.describe Nav::TopNavHelper do
it 'has admin as first :secondary item' do
expected_admin_item = ::Gitlab::Nav::TopNavMenuItem.build(
data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Admin',
+ qa_selector: 'admin_area_link',
**menu_data_tracking_attrs('admin')
},
id: 'admin',
title: 'Admin',
icon: 'admin',
- href: '/admin',
- css_class: 'qa-admin-area-link'
+ href: '/admin'
)
expect(subject[:secondary].first).to eq(expected_admin_item)
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index 1e16d969744..34d7cadf048 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -108,8 +108,8 @@ RSpec.describe PageLayoutHelper do
tags = helper.page_card_meta_tags
aggregate_failures do
- expect(tags).to include %q(<meta property="twitter:label1" content="foo" />)
- expect(tags).to include %q(<meta property="twitter:data1" content="bar" />)
+ expect(tags).to include %q(<meta property="twitter:label1" content="foo">)
+ expect(tags).to include %q(<meta property="twitter:data1" content="bar">)
end
end
diff --git a/spec/helpers/preferred_language_switcher_helper_spec.rb b/spec/helpers/preferred_language_switcher_helper_spec.rb
new file mode 100644
index 00000000000..aab65ecc210
--- /dev/null
+++ b/spec/helpers/preferred_language_switcher_helper_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PreferredLanguageSwitcherHelper do
+ include StubLanguagesTranslationPercentage
+
+ describe '#ordered_selectable_locales' do
+ before do
+ stub_languages_translation_percentage(es: 65, en: 100, zh_CN: described_class::SWITCHER_MINIMUM_TRANSLATION_LEVEL)
+ end
+
+ it 'returns filtered and ordered by translation level selectable locales' do
+ expect(helper.ordered_selectable_locales).to eq(
+ [
+ { value: 'en', text: 'English', percentage: 100 },
+ { value: 'zh_CN', text: "简体中文", percentage: described_class::SWITCHER_MINIMUM_TRANSLATION_LEVEL }
+ ]
+ )
+ end
+ end
+end
diff --git a/spec/helpers/programming_languages_helper_spec.rb b/spec/helpers/programming_languages_helper_spec.rb
new file mode 100644
index 00000000000..bbea48be64d
--- /dev/null
+++ b/spec/helpers/programming_languages_helper_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProgrammingLanguagesHelper do
+ describe '.search_language_placeholder' do
+ let(:programming_language) { build(:programming_language, id: 1, name: 'Ruby') }
+
+ before do
+ allow(helper).to receive(:programming_languages).and_return([programming_language])
+ end
+
+ context 'with no `language` param' do
+ it 'returns a placeholder' do
+ expect(helper.search_language_placeholder).to eq(_('Language'))
+ end
+ end
+
+ context 'with a `language` param' do
+ before do
+ allow(helper).to receive(:params).and_return({ language: '2' })
+ end
+
+ context 'when invalid' do
+ it 'returns a placeholder' do
+ expect(helper.search_language_placeholder).to eq(_('Language'))
+ end
+ end
+
+ context 'when valid' do
+ let(:programming_language) { build(:programming_language, id: 2, name: 'Ruby') }
+
+ it 'returns the chosen language' do
+ expect(helper.search_language_placeholder).to eq('Ruby')
+ end
+ end
+ end
+ end
+
+ describe '.programming_languages' do
+ it 'callings ProgrammingLanguage.most_popular' do
+ expect(ProgrammingLanguage).to receive(:most_popular)
+
+ helper.programming_languages
+ end
+ end
+
+ describe '.language_state_class' do
+ let(:language) { build(:programming_language, id: language_id) }
+
+ before do
+ allow(helper).to receive(:params).and_return({ language: '1' })
+ end
+
+ context 'when language param matches' do
+ let(:language_id) { 1 }
+
+ it 'returns `is-active`' do
+ expect(helper.language_state_class(language)).to be('is-active')
+ end
+ end
+
+ context 'when language param does not match' do
+ let(:language_id) { 2 }
+
+ it 'returns ``' do
+ expect(helper.language_state_class(language)).to be('')
+ end
+ end
+ end
+end
diff --git a/spec/helpers/projects/ml/experiments_helper_spec.rb b/spec/helpers/projects/ml/experiments_helper_spec.rb
index e4421ff7606..e6959a03c4a 100644
--- a/spec/helpers/projects/ml/experiments_helper_spec.rb
+++ b/spec/helpers/projects/ml/experiments_helper_spec.rb
@@ -5,28 +5,36 @@ require 'rspec'
require 'spec_helper'
require 'mime/types'
-RSpec.describe Projects::Ml::ExperimentsHelper do
- let_it_be(:project) { build(:project, :private) }
- let_it_be(:experiment) { build(:ml_experiments, user_id: project.creator, project: project) }
- let_it_be(:candidates) do
- create_list(:ml_candidates, 2, experiment: experiment, user: project.creator).tap do |c|
- c[0].params.create!([{ name: 'param1', value: 'p1' }, { name: 'param2', value: 'p2' }])
- c[0].metrics.create!(
+RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:experiment) { create(:ml_experiments, user_id: project.creator, project: project) }
+ let_it_be(:candidate0) do
+ create(:ml_candidates, experiment: experiment, user: project.creator).tap do |c|
+ c.params.build([{ name: 'param1', value: 'p1' }, { name: 'param2', value: 'p2' }])
+ c.metrics.create!(
[{ name: 'metric1', value: 0.1 }, { name: 'metric2', value: 0.2 }, { name: 'metric3', value: 0.3 }]
)
+ end
+ end
- c[1].params.create!([{ name: 'param2', value: 'p3' }, { name: 'param3', value: 'p4' }])
- c[1].metrics.create!(name: 'metric3', value: 0.4)
+ let_it_be(:candidate1) do
+ create(:ml_candidates, experiment: experiment, user: project.creator).tap do |c|
+ c.params.build([{ name: 'param2', value: 'p3' }, { name: 'param3', value: 'p4' }])
+ c.metrics.create!(name: 'metric3', value: 0.4)
end
end
+ let_it_be(:candidates) { [candidate0, candidate1] }
+
describe '#candidates_table_items' do
subject { helper.candidates_table_items(candidates) }
it 'creates the correct model for the table' do
expected_value = [
- { 'param1' => 'p1', 'param2' => 'p2', 'metric1' => '0.1000', 'metric2' => '0.2000', 'metric3' => '0.3000' },
- { 'param2' => 'p3', 'param3' => 'p4', 'metric3' => '0.4000' }
+ { 'param1' => 'p1', 'param2' => 'p2', 'metric1' => '0.1000', 'metric2' => '0.2000', 'metric3' => '0.3000',
+ 'artifact' => nil, 'details' => "/#{project.full_path}/-/ml/candidates/#{candidate0.iid}" },
+ { 'param2' => 'p3', 'param3' => 'p4', 'metric3' => '0.4000',
+ 'artifact' => nil, 'details' => "/#{project.full_path}/-/ml/candidates/#{candidate1.iid}" }
]
expect(Gitlab::Json.parse(subject)).to match_array(expected_value)
@@ -46,4 +54,40 @@ RSpec.describe Projects::Ml::ExperimentsHelper do
it { is_expected.to match_array(%w[metric1 metric2 metric3]) }
end
end
+
+ describe '#candidate_as_data' do
+ let(:candidate) { candidate0 }
+ let(:package) do
+ create(:generic_package, name: candidate.package_name, version: candidate.package_version, project: project)
+ end
+
+ subject { Gitlab::Json.parse(helper.candidate_as_data(candidate)) }
+
+ it 'generates the correct params' do
+ expect(subject['params']).to include(
+ hash_including('name' => 'param1', 'value' => 'p1'),
+ hash_including('name' => 'param2', 'value' => 'p2')
+ )
+ end
+
+ it 'generates the correct metrics' do
+ expect(subject['metrics']).to include(
+ hash_including('name' => 'metric1', 'value' => 0.1),
+ hash_including('name' => 'metric2', 'value' => 0.2),
+ hash_including('name' => 'metric3', 'value' => 0.3)
+ )
+ end
+
+ it 'generates the correct info' do
+ expected_info = {
+ 'iid' => candidate.iid,
+ 'path_to_artifact' => "/#{project.full_path}/-/packages/#{package.id}",
+ 'experiment_name' => candidate.experiment.name,
+ 'path_to_experiment' => "/#{project.full_path}/-/ml/experiments/#{experiment.iid}",
+ 'status' => 'running'
+ }
+
+ expect(subject['info']).to include(expected_info)
+ end
+ end
end
diff --git a/spec/helpers/projects/pipeline_helper_spec.rb b/spec/helpers/projects/pipeline_helper_spec.rb
index 0d3466d6ed2..35045aaef2a 100644
--- a/spec/helpers/projects/pipeline_helper_spec.rb
+++ b/spec/helpers/projects/pipeline_helper_spec.rb
@@ -25,6 +25,7 @@ RSpec.describe Projects::PipelineHelper do
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json),
pipeline_iid: pipeline.iid,
+ pipeline_path: pipeline_path(pipeline),
pipeline_project_path: project.full_path,
total_job_count: pipeline.total_size,
summary_endpoint: summary_project_pipeline_tests_path(project, pipeline, format: :json),
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 39b8b552672..db50c74ec4e 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -404,10 +404,6 @@ RSpec.describe ProjectsHelper do
Project.all
end
- before do
- stub_feature_flags(project_list_filter_bar: false)
- end
-
it 'returns true when there are projects' do
expect(helper.show_projects?(projects, {})).to eq(true)
end
@@ -963,7 +959,6 @@ RSpec.describe ProjectsHelper do
lfsEnabled: !!project.lfs_enabled,
emailsDisabled: project.emails_disabled?,
metricsDashboardAccessLevel: project.project_feature.metrics_dashboard_access_level,
- operationsAccessLevel: project.project_feature.operations_access_level,
showDefaultAwardEmojis: project.show_default_award_emojis?,
securityAndComplianceAccessLevel: project.security_and_compliance_access_level,
containerRegistryAccessLevel: project.project_feature.container_registry_access_level,
@@ -1338,6 +1333,27 @@ RSpec.describe ProjectsHelper do
end
end
+ describe '#fork_divergence_message' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:behind, :ahead, :message) do
+ 0 | 0 | 'Up to date with upstream repository'
+ 1 | 0 | '1 commit behind upstream repository'
+ 2 | 0 | '2 commits behind upstream repository'
+ 0 | 1 | '1 commit ahead of upstream repository'
+ 0 | 2 | '2 commits ahead of upstream repository'
+ 5 | 7 | '5 commits behind, 7 commits ahead of upstream repository'
+ nil | 7 | 'Fork has diverged from upstream repository'
+ 7 | nil | 'Fork has diverged from upstream repository'
+ end
+
+ with_them do
+ it 'returns message based on behind/ahead values' do
+ expect(helper.fork_divergence_message({ behind: behind, ahead: ahead })).to eq(message)
+ end
+ end
+ end
+
describe '#localized_project_human_access' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 192dfaa9caf..45864320115 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SearchHelper do
+RSpec.describe SearchHelper, feature_category: :global_search do
include MarkupHelper
include BadgesHelper
@@ -60,6 +60,44 @@ RSpec.describe SearchHelper do
expect(search_autocomplete_opts(project.name).size).to eq(1)
end
+ context 'for users' do
+ let_it_be(:another_user) { create(:user, name: 'Jane Doe') }
+ let(:term) { 'jane' }
+
+ it 'makes a call to SearchService' do
+ params = { search: term, per_page: 5, scope: 'users' }
+ expect(SearchService).to receive(:new).with(current_user, params).and_call_original
+
+ search_autocomplete_opts(term)
+ end
+
+ it 'returns users matching the term' do
+ result = search_autocomplete_opts(term)
+ expect(result.size).to eq(1)
+ expect(result.first[:id]).to eq(another_user.id)
+ end
+
+ context 'when current_user cannot read_users_list' do
+ before do
+ allow(Ability).to receive(:allowed?).and_return(true)
+ allow(Ability).to receive(:allowed?).with(current_user, :read_users_list).and_return(false)
+ end
+
+ it 'returns an empty array' do
+ expect(search_autocomplete_opts(term)).to eq([])
+ end
+ end
+
+ context 'with limiting' do
+ let!(:users) { create_list(:user, 6, name: 'Jane Doe') }
+
+ it 'only returns the first 5 users' do
+ result = search_autocomplete_opts(term)
+ expect(result.size).to eq(5)
+ end
+ end
+ end
+
it "includes the required project attrs" do
project = create(:project, namespace: create(:namespace, owner: user))
result = search_autocomplete_opts(project.name).first
@@ -858,17 +896,18 @@ RSpec.describe SearchHelper do
end
context 'code' do
- where(:feature_flag_tab_enabled, :show_elasticsearch_tabs, :project_search_tabs, :condition) do
- false | false | false | false
- true | true | true | true
- true | false | false | false
- false | true | false | false
- false | false | true | true
- true | false | true | true
+ where(:feature_flag_tab_enabled, :show_elasticsearch_tabs, :global_project, :project_search_tabs, :condition) do
+ false | false | nil | false | false
+ true | true | nil | true | true
+ true | false | nil | false | false
+ false | true | nil | false | false
+ false | false | ref(:project) | true | true
+ true | false | ref(:project) | false | false
end
with_them do
it 'data item condition is set correctly' do
+ @project = global_project
allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(show_elasticsearch_tabs)
allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_code_tab).and_return(feature_flag_tab_enabled)
allow(self).to receive(:project_search_tabs?).with(:blobs).and_return(project_search_tabs)
@@ -879,16 +918,16 @@ RSpec.describe SearchHelper do
end
context 'issues' do
- where(:feature_flag_tab_enabled, :project_search_tabs, :condition) do
- false | false | false
- true | true | true
- true | false | true
- false | true | true
+ where(:project_search_tabs, :global_search_issues_tab, :condition) do
+ false | false | false
+ false | true | true
+ true | false | true
+ true | true | true
end
with_them do
it 'data item condition is set correctly' do
- allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_issues_tab).and_return(feature_flag_tab_enabled)
+ allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_issues_tab).and_return(global_search_issues_tab)
allow(self).to receive(:project_search_tabs?).with(:issues).and_return(project_search_tabs)
expect(search_navigation[:issues][:condition]).to eq(condition)
@@ -897,11 +936,11 @@ RSpec.describe SearchHelper do
end
context 'merge requests' do
- where(:feature_flag_tab_enabled, :project_search_tabs, :condition) do
- false | false | false
- true | true | true
- true | false | true
- false | true | true
+ where(:project_search_tabs, :feature_flag_tab_enabled, :condition) do
+ false | false | false
+ true | false | true
+ false | true | true
+ true | true | true
end
with_them do
@@ -915,16 +954,19 @@ RSpec.describe SearchHelper do
end
context 'wiki' do
- where(:project_search_tabs, :show_elasticsearch_tabs, :condition) do
- false | false | false
- true | true | true
- true | false | true
- false | true | true
+ where(:global_search_wiki_tab, :show_elasticsearch_tabs, :global_project, :project_search_tabs, :condition) do
+ false | false | nil | true | true
+ false | false | nil | false | false
+ false | true | nil | false | false
+ true | false | nil | false | false
+ true | true | ref(:project) | false | false
end
with_them do
it 'data item condition is set correctly' do
+ @project = global_project
allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(show_elasticsearch_tabs)
+ allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_wiki_tab).and_return(global_search_wiki_tab)
allow(self).to receive(:project_search_tabs?).with(:wiki).and_return(project_search_tabs)
expect(search_navigation[:wiki_blobs][:condition]).to eq(condition)
@@ -933,17 +975,20 @@ RSpec.describe SearchHelper do
end
context 'commits' do
- where(:feature_flag_tab_enabled, :show_elasticsearch_tabs, :project_search_tabs, :condition) do
- false | false | false | false
- true | true | true | true
- true | false | false | false
- false | true | true | true
+ where(:global_search_commits_tab, :show_elasticsearch_tabs, :global_project, :project_search_tabs, :condition) do
+ false | false | nil | true | true
+ false | false | nil | false | false
+ false | true | nil | false | false
+ true | false | nil | false | false
+ true | true | ref(:project) | false | false
+ true | true | nil | false | true
end
with_them do
it 'data item condition is set correctly' do
+ @project = global_project
allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(show_elasticsearch_tabs)
- allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_commits_tab).and_return(feature_flag_tab_enabled)
+ allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_commits_tab).and_return(global_search_commits_tab)
allow(self).to receive(:project_search_tabs?).with(:commits).and_return(project_search_tabs)
expect(search_navigation[:commits][:condition]).to eq(condition)
@@ -952,11 +997,11 @@ RSpec.describe SearchHelper do
end
context 'comments' do
- where(:show_elasticsearch_tabs, :project_search_tabs, :condition) do
- true | true | true
- false | false | false
- true | false | true
- false | true | true
+ where(:project_search_tabs, :show_elasticsearch_tabs, :condition) do
+ true | true | true
+ false | false | false
+ false | true | true
+ true | false | true
end
with_them do
@@ -1012,7 +1057,7 @@ RSpec.describe SearchHelper do
with_them do
it 'data item condition is set correctly' do
- @show_snippets = global_show_snippets
+ allow(search_service).to receive(:show_snippets?).and_return(global_show_snippets)
@project = global_project
expect(search_navigation[:snippet_titles][:condition]).to eq(condition)
@@ -1063,9 +1108,9 @@ RSpec.describe SearchHelper do
allow(self).to receive(:can?).and_return(true)
allow(self).to receive(:project_search_tabs?).and_return(true)
allow(self).to receive(:feature_flag_tab_enabled?).and_return(true)
- allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(true)
allow(self).to receive(:feature_flag_tab_enabled?).and_return(true)
- @show_snippets = true
+ allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(true)
+ allow(search_service).to receive(:show_snippets?).and_return(true)
@project = nil
end
diff --git a/spec/helpers/sorting_helper_spec.rb b/spec/helpers/sorting_helper_spec.rb
index 3e555301325..d561b08efac 100644
--- a/spec/helpers/sorting_helper_spec.rb
+++ b/spec/helpers/sorting_helper_spec.rb
@@ -109,103 +109,14 @@ RSpec.describe SortingHelper do
describe '#projects_sort_options_hash' do
it 'returns a hash of available sorting options' do
- expect(projects_sort_options_hash).to include(project_common_options)
- end
- end
-
- describe '#projects_reverse_sort_options_hash' do
- context 'returns a reversed hash of available sorting options' do
- using RSpec::Parameterized::TableSyntax
-
- where(:sort_key, :reverse_sort_title) do
- sort_value_latest_activity | sort_value_oldest_activity
- sort_value_recently_created | sort_value_oldest_created
- sort_value_name | sort_value_name_desc
- sort_value_stars_desc | sort_value_stars_asc
- sort_value_oldest_activity | sort_value_latest_activity
- sort_value_oldest_created | sort_value_recently_created
- sort_value_name_desc | sort_value_name
- sort_value_stars_asc | sort_value_stars_desc
- end
-
- with_them do
- it do
- reverse_hash = projects_reverse_sort_options_hash
-
- expect(reverse_hash).to include(sort_key)
- expect(reverse_hash[sort_key]).to eq(reverse_sort_title)
- end
- end
- end
- end
-
- describe '#project_sort_direction_button' do
- context 'returns the correct icon for each sort option' do
- using RSpec::Parameterized::TableSyntax
-
- sort_lowest_icon = 'sort-lowest'
- sort_highest_icon = 'sort-highest'
-
- where(:selected_sort, :icon) do
- sort_value_latest_activity | sort_highest_icon
- sort_value_recently_created | sort_highest_icon
- sort_value_name_desc | sort_highest_icon
- sort_value_stars_desc | sort_highest_icon
- sort_value_oldest_activity | sort_lowest_icon
- sort_value_oldest_created | sort_lowest_icon
- sort_value_name | sort_lowest_icon
- sort_value_stars_asc | sort_lowest_icon
- end
-
- with_them do
- it do
- set_sorting_url selected_sort
-
- expect(project_sort_direction_button(selected_sort)).to include(icon)
- end
- end
- end
-
- it 'returns the correct link to reverse the current sort option' do
- sort_options_links = projects_reverse_sort_options_hash
-
- sort_options_links.each do |selected_sort, reverse_sort|
- set_sorting_url selected_sort
-
- expect(project_sort_direction_button(selected_sort)).to include(reverse_sort)
- end
- end
- end
-
- describe '#projects_sort_option_titles' do
- it 'returns a hash of titles for the sorting options' do
options = project_common_options.merge({
- sort_value_oldest_activity => sort_title_latest_activity,
- sort_value_oldest_created => sort_title_created_date,
- sort_value_name_desc => sort_title_name,
- sort_value_stars_asc => sort_title_stars
+ sort_value_oldest_activity => sort_title_oldest_activity,
+ sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_recently_created => sort_title_recently_created,
+ sort_value_stars_desc => sort_title_most_stars
})
- expect(projects_sort_option_titles).to eq(options)
- end
- end
-
- describe 'with project_list_filter_bar off' do
- before do
- stub_feature_flags(project_list_filter_bar: false)
- end
-
- describe '#projects_sort_options_hash' do
- it 'returns a hash of available sorting options' do
- options = project_common_options.merge({
- sort_value_oldest_activity => sort_title_oldest_activity,
- sort_value_oldest_created => sort_title_oldest_created,
- sort_value_recently_created => sort_title_recently_created,
- sort_value_stars_desc => sort_title_most_stars
- })
-
- expect(projects_sort_options_hash).to eq(options)
- end
+ expect(projects_sort_options_hash).to eq(options)
end
end
end
diff --git a/spec/helpers/tab_helper_spec.rb b/spec/helpers/tab_helper_spec.rb
index 80a1224abbb..5b74eda34cd 100644
--- a/spec/helpers/tab_helper_spec.rb
+++ b/spec/helpers/tab_helper_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe TabHelper do
describe 'gl_tabs_nav' do
it 'creates a tabs navigation' do
- expect(helper.gl_tabs_nav).to match(%r{<ul class="nav gl-tabs-nav"><\/ul>})
+ expect(helper.gl_tabs_nav).to match(%r{<ul class="nav gl-tabs-nav"></ul>})
end
it 'captures block output' do
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
index 7c91dd0570f..ca334a04fe9 100644
--- a/spec/helpers/todos_helper_spec.rb
+++ b/spec/helpers/todos_helper_spec.rb
@@ -14,6 +14,8 @@ RSpec.describe TodosHelper do
note: 'I am note, hear me roar')
end
+ let_it_be(:group) { create(:group, :public, name: 'Group 1') }
+
let_it_be(:design_todo) do
create(:todo, :mentioned,
user: user,
@@ -37,6 +39,10 @@ RSpec.describe TodosHelper do
create(:todo, target: issue)
end
+ let_it_be(:group_todo) do
+ create(:todo, target: group)
+ end
+
describe '#todos_count_format' do
it 'shows fuzzy count for 100 or more items' do
expect(helper.todos_count_format(100)).to eq '99+'
@@ -50,16 +56,14 @@ RSpec.describe TodosHelper do
end
end
- describe '#todo_target_link' do
+ describe '#todo_target_name' do
context 'when given a design' do
let(:todo) { design_todo }
- it 'produces a good link' do
- path = helper.todo_target_path(todo)
- link = helper.todo_target_link(todo)
- expected = "<a href=\"#{path}\">design #{design.to_reference}</a>"
+ it 'references the filename of the design' do
+ name = helper.todo_target_name(todo)
- expect(link).to eq(expected)
+ expect(name).to eq(design.to_reference.to_s)
end
end
end
@@ -94,7 +98,7 @@ RSpec.describe TodosHelper do
it 'returns the title' do
title = helper.todo_target_title(todo)
- expect(title).to eq("\"Issue 1\"")
+ expect(title).to eq("Issue 1")
end
end
end
@@ -155,33 +159,51 @@ RSpec.describe TodosHelper do
expect(path).to eq("/#{issue.project.full_path}/-/issues/#{issue.iid}##{dom_id(note)}")
end
end
+
+ context 'when a user requests access to group' do
+ let_it_be(:group_access_request_todo) do
+ create(:todo,
+ target_id: group.id,
+ target_type: group.class.polymorphic_name,
+ group: group,
+ action: Todo::MEMBER_ACCESS_REQUESTED)
+ end
+
+ it 'responds with access requests tab' do
+ path = helper.todo_target_path(group_access_request_todo)
+
+ access_request_path = Gitlab::Routing.url_helpers.group_group_members_url(group, tab: 'access_requests')
+
+ expect(path).to eq(access_request_path)
+ end
+ end
end
- describe '#todo_target_type_name' do
- subject { helper.todo_target_type_name(todo) }
+ describe '#todo_target_aria_label' do
+ subject { helper.todo_target_aria_label(todo) }
context 'when given a design todo' do
let(:todo) { design_todo }
- it { is_expected.to eq('design') }
+ it { is_expected.to eq("Design ##{todo.target.iid}[#{todo.target.title}]") }
end
context 'when given an alert todo' do
let(:todo) { alert_todo }
- it { is_expected.to eq('alert') }
+ it { is_expected.to eq("Alert ^alert##{todo.target.iid}") }
end
context 'when given a task todo' do
let(:todo) { task_todo }
- it { is_expected.to eq('task') }
+ it { is_expected.to eq("Task ##{todo.target.iid}") }
end
context 'when given an issue todo' do
let(:todo) { issue_todo }
- it { is_expected.to eq('issue') }
+ it { is_expected.to eq("Issue ##{todo.target.iid}") }
end
context 'when given a merge request todo' do
@@ -190,7 +212,7 @@ RSpec.describe TodosHelper do
create(:todo, target: merge_request)
end
- it { is_expected.to eq('merge request') }
+ it { is_expected.to eq("Merge Request !#{todo.target.iid}") }
end
end
@@ -229,7 +251,7 @@ RSpec.describe TodosHelper do
todo.target.update!(state: 'closed')
end
- it_behaves_like 'a rendered state pill', css: '.gl-bg-red-500', state: 'closed'
+ it_behaves_like 'a rendered state pill', css: '.badge-danger', state: 'closed'
end
context 'merged MR' do
@@ -237,7 +259,7 @@ RSpec.describe TodosHelper do
todo.target.update!(state: 'merged')
end
- it_behaves_like 'a rendered state pill', css: '.gl-bg-blue-500', state: 'merged'
+ it_behaves_like 'a rendered state pill', css: '.badge-info', state: 'merged'
end
end
@@ -251,7 +273,7 @@ RSpec.describe TodosHelper do
todo.target.update!(state: 'closed')
end
- it_behaves_like 'a rendered state pill', css: '.gl-bg-blue-500', state: 'closed'
+ it_behaves_like 'a rendered state pill', css: '.badge-info', state: 'closed'
end
end
@@ -265,7 +287,7 @@ RSpec.describe TodosHelper do
todo.target.resolve!
end
- it_behaves_like 'a rendered state pill', css: '.gl-bg-blue-500', state: 'resolved'
+ it_behaves_like 'a rendered state pill', css: '.badge-info', state: 'resolved'
end
end
end
@@ -329,17 +351,17 @@ RSpec.describe TodosHelper do
where(:action, :self_added?, :expected_action_name) do
Todo::ASSIGNED | false | s_('Todos|assigned you')
Todo::ASSIGNED | true | s_('Todos|assigned')
- Todo::REVIEW_REQUESTED | true | s_('Todos|requested a review of')
- Todo::MENTIONED | true | format(s_("Todos|mentioned %{who} on"), who: s_('Todos|yourself'))
- Todo::MENTIONED | false | format(s_("Todos|mentioned %{who} on"), who: _('you'))
- Todo::DIRECTLY_ADDRESSED | true | format(s_("Todos|mentioned %{who} on"), who: s_('Todos|yourself'))
- Todo::DIRECTLY_ADDRESSED | false | format(s_("Todos|mentioned %{who} on"), who: _('you'))
- Todo::BUILD_FAILED | true | s_('Todos|The pipeline failed in')
- Todo::MARKED | true | s_('Todos|added a todo for')
- Todo::APPROVAL_REQUIRED | true | format(s_("Todos|set %{who} as an approver for"), who: s_('Todos|yourself'))
- Todo::APPROVAL_REQUIRED | false | format(s_("Todos|set %{who} as an approver for"), who: _('you'))
+ Todo::REVIEW_REQUESTED | true | s_('Todos|requested a review')
+ Todo::MENTIONED | true | format(s_("Todos|mentioned %{who}"), who: s_('Todos|yourself'))
+ Todo::MENTIONED | false | format(s_("Todos|mentioned %{who}"), who: _('you'))
+ Todo::DIRECTLY_ADDRESSED | true | format(s_("Todos|mentioned %{who}"), who: s_('Todos|yourself'))
+ Todo::DIRECTLY_ADDRESSED | false | format(s_("Todos|mentioned %{who}"), who: _('you'))
+ Todo::BUILD_FAILED | true | s_('Todos|The pipeline failed')
+ Todo::MARKED | true | s_('Todos|added a to-do item')
+ Todo::APPROVAL_REQUIRED | true | format(s_("Todos|set %{who} as an approver"), who: s_('Todos|yourself'))
+ Todo::APPROVAL_REQUIRED | false | format(s_("Todos|set %{who} as an approver"), who: _('you'))
Todo::UNMERGEABLE | true | s_('Todos|Could not merge')
- Todo::MERGE_TRAIN_REMOVED | true | s_("Todos|Removed from Merge Train:")
+ Todo::MERGE_TRAIN_REMOVED | true | s_("Todos|Removed from Merge Train")
end
with_them do
@@ -350,5 +372,45 @@ RSpec.describe TodosHelper do
it { expect(helper.todo_action_name(alert_todo)).to eq(expected_action_name) }
end
+
+ context 'member access requested' do
+ context 'when source is group' do
+ it 'returns group access message' do
+ group_todo.action = Todo::MEMBER_ACCESS_REQUESTED
+
+ expect(helper.todo_action_name(group_todo)).to eq(
+ format(s_("Todos|has requested access to group %{which}"), which: _(group.name))
+ )
+ end
+ end
+ end
+ end
+
+ describe '#todo_due_date' do
+ subject(:result) { helper.todo_due_date(todo) }
+
+ context 'due date is today' do
+ let_it_be(:issue_with_today_due_date) do
+ create(:issue, title: 'Issue 1', project: project, due_date: Date.current)
+ end
+
+ let(:todo) do
+ create(:todo, project: issue_with_today_due_date.project, target: issue_with_today_due_date, note: note)
+ end
+
+ it { expect(result).to match('Due today') }
+ end
+
+ context 'due date is not today' do
+ let_it_be(:issue_with_tomorrow_due_date) do
+ create(:issue, title: 'Issue 1', project: project, due_date: Date.tomorrow)
+ end
+
+ let(:todo) do
+ create(:todo, project: issue_with_tomorrow_due_date.project, target: issue_with_tomorrow_due_date, note: note)
+ end
+
+ it { expect(result).to match("Due #{l(Date.tomorrow, format: Date::DATE_FORMATS[:medium])}") }
+ end
end
end
diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
index 959c4a94a78..2bb85e7b6b8 100644
--- a/spec/helpers/version_check_helper_spec.rb
+++ b/spec/helpers/version_check_helper_spec.rb
@@ -34,4 +34,42 @@ RSpec.describe VersionCheckHelper do
end
end
end
+
+ describe '#gitlab_version_check' do
+ before do
+ allow_next_instance_of(VersionCheck) do |instance|
+ allow(instance).to receive(:response).and_return({ "severity" => "success" })
+ end
+ end
+
+ it 'returns an instance of the VersionCheck class' do
+ expect(helper.gitlab_version_check).to eq({ "severity" => "success" })
+ end
+ end
+
+ describe '#show_security_patch_upgrade_alert?' do
+ describe 'return conditions' do
+ where(:show_version_check, :gitlab_version_check, :result) do
+ [
+ [false, nil, false],
+ [false, { "severity" => "success" }, false],
+ [false, { "severity" => "danger" }, false],
+ [true, nil, false],
+ [true, { "severity" => "success" }, false],
+ [true, { "severity" => "danger" }, true]
+ ]
+ end
+
+ with_them do
+ before do
+ allow(helper).to receive(:show_version_check?).and_return(show_version_check)
+ allow(helper).to receive(:gitlab_version_check).and_return(gitlab_version_check)
+ end
+
+ it 'returns correct results' do
+ expect(helper.show_security_patch_upgrade_alert?).to eq result
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/web_hooks/web_hooks_helper_spec.rb b/spec/helpers/web_hooks/web_hooks_helper_spec.rb
index 473f33a982f..bcd9d2df1dc 100644
--- a/spec/helpers/web_hooks/web_hooks_helper_spec.rb
+++ b/spec/helpers/web_hooks/web_hooks_helper_spec.rb
@@ -7,39 +7,16 @@ RSpec.describe WebHooks::WebHooksHelper do
let(:current_user) { nil }
let(:callout_dismissed) { false }
- let(:web_hooks_disable_failed) { false }
- let(:webhooks_failed_callout) { false }
before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:web_hook_disabled_dismissed?).with(project).and_return(callout_dismissed)
-
- stub_feature_flags(
- webhooks_failed_callout: webhooks_failed_callout,
- web_hooks_disable_failed: web_hooks_disable_failed
- )
end
shared_context 'user is logged in' do
let(:current_user) { create(:user) }
end
- shared_context 'webhooks_failed_callout is enabled' do
- let(:webhooks_failed_callout) { true }
- end
-
- shared_context 'webhooks_failed_callout is enabled for this project' do
- let(:webhooks_failed_callout) { project }
- end
-
- shared_context 'web_hooks_disable_failed is enabled' do
- let(:web_hooks_disable_failed) { true }
- end
-
- shared_context 'web_hooks_disable_failed is enabled for this project' do
- let(:web_hooks_disable_failed) { project }
- end
-
shared_context 'the user has permission' do
before do
project.add_maintainer(current_user)
@@ -59,8 +36,6 @@ RSpec.describe WebHooks::WebHooksHelper do
describe '#show_project_hook_failed_callout?' do
context 'all conditions are met' do
include_context 'user is logged in'
- include_context 'webhooks_failed_callout is enabled'
- include_context 'web_hooks_disable_failed is enabled'
include_context 'the user has permission'
include_context 'a hook has failed'
@@ -85,23 +60,9 @@ RSpec.describe WebHooks::WebHooksHelper do
end
end
- context 'all conditions are met, project scoped flags' do
- include_context 'user is logged in'
- include_context 'webhooks_failed_callout is enabled for this project'
- include_context 'web_hooks_disable_failed is enabled for this project'
- include_context 'the user has permission'
- include_context 'a hook has failed'
-
- it 'is true' do
- expect(helper).to be_show_project_hook_failed_callout(project: project)
- end
- end
-
context 'one condition is not met' do
contexts = [
'user is logged in',
- 'webhooks_failed_callout is enabled',
- 'web_hooks_disable_failed is enabled',
'the user has permission',
'a hook has failed'
]
diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb
index 59624dc0682..497cd5d1e7f 100644
--- a/spec/helpers/wiki_helper_spec.rb
+++ b/spec/helpers/wiki_helper_spec.rb
@@ -76,7 +76,7 @@ RSpec.describe WikiHelper do
describe '#wiki_sort_controls' do
let(:wiki) { create(:project_wiki) }
let(:wiki_link) { helper.wiki_sort_controls(wiki, direction) }
- let(:classes) { "gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn qa-reverse-sort rspec-reverse-sort" }
+ let(:classes) { "gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn rspec-reverse-sort" }
def expected_link(direction, icon_class)
path = "/#{wiki.project.full_path}/-/wikis/pages?direction=#{direction}"
diff --git a/spec/helpers/x509_helper_spec.rb b/spec/helpers/x509_helper_spec.rb
index 4e3e8c8d3f6..dfe9259bd0f 100644
--- a/spec/helpers/x509_helper_spec.rb
+++ b/spec/helpers/x509_helper_spec.rb
@@ -57,22 +57,4 @@ RSpec.describe X509Helper do
end
end
end
-
- describe '#x509_signature?' do
- let(:x509_signature) { create(:x509_commit_signature) }
- let(:gpg_signature) { create(:gpg_signature) }
-
- it 'detects a x509 signed commit' do
- signature = Gitlab::X509::Signature.new(
- X509Helpers::User1.signed_commit_signature,
- X509Helpers::User1.signed_commit_base_data,
- X509Helpers::User1.certificate_email,
- X509Helpers::User1.signed_commit_time
- )
-
- expect(x509_signature?(x509_signature)).to be_truthy
- expect(x509_signature?(signature)).to be_truthy
- expect(x509_signature?(gpg_signature)).to be_falsey
- end
- end
end
diff --git a/spec/initializers/database_config_spec.rb b/spec/initializers/database_config_spec.rb
index 230f1296760..bbb5e7b1923 100644
--- a/spec/initializers/database_config_spec.rb
+++ b/spec/initializers/database_config_spec.rb
@@ -7,15 +7,33 @@ RSpec.describe 'Database config initializer', :reestablished_active_record_base
load Rails.root.join('config/initializers/database_config.rb')
end
- it 'retains the correct database name for the connection' do
- previous_db_name = ApplicationRecord.connection.pool.db_config.name
+ shared_examples 'does not change connection attributes' do
+ it 'retains the correct database name for connection' do
+ previous_db_name = database_base_model.connection.pool.db_config.name
- subject
+ subject
- expect(ApplicationRecord.connection.pool.db_config.name).to eq(previous_db_name)
+ expect(database_base_model.connection.pool.db_config.name).to eq(previous_db_name)
+ end
+
+ it 'does not overwrite custom pool settings' do
+ expect { subject }.not_to change { database_base_model.connection_db_config.pool }
+ end
+ end
+
+ context 'when main database connection' do
+ let(:database_base_model) { Gitlab::Database.database_base_models[:main] }
+
+ it_behaves_like 'does not change connection attributes'
end
- it 'does not overwrite custom pool settings' do
- expect { subject }.not_to change { ActiveRecord::Base.connection_db_config.pool }
+ context 'when ci database connection' do
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ let(:database_base_model) { Gitlab::Database.database_base_models[:ci] }
+
+ it_behaves_like 'does not change connection attributes'
end
end
diff --git a/spec/initializers/diagnostic_reports_spec.rb b/spec/initializers/diagnostic_reports_spec.rb
index 01b1ed9b7b5..dc989efe809 100644
--- a/spec/initializers/diagnostic_reports_spec.rb
+++ b/spec/initializers/diagnostic_reports_spec.rb
@@ -2,15 +2,17 @@
require 'spec_helper'
-RSpec.describe 'diagnostic reports' do
+RSpec.describe 'diagnostic reports', :aggregate_failures, feature_category: :application_performance do
subject(:load_initializer) do
load Rails.root.join('config/initializers/diagnostic_reports.rb')
end
- shared_examples 'does not modify worker startup hooks' do
+ shared_examples 'does not modify worker hooks' do
it do
expect(Gitlab::Cluster::LifecycleEvents).not_to receive(:on_worker_start)
+ expect(Gitlab::Cluster::LifecycleEvents).not_to receive(:on_worker_stop)
expect(Gitlab::Memory::ReportsDaemon).not_to receive(:instance)
+ expect(Gitlab::Memory::Reporter).not_to receive(:new)
load_initializer
end
@@ -27,21 +29,27 @@ RSpec.describe 'diagnostic reports' do
end
let(:report_daemon) { instance_double(Gitlab::Memory::ReportsDaemon) }
+ let(:reporter) { instance_double(Gitlab::Memory::Reporter) }
it 'modifies worker startup hooks, starts Gitlab::Memory::ReportsDaemon' do
expect(Gitlab::Cluster::LifecycleEvents).to receive(:on_worker_start).and_call_original
-
+ expect(Gitlab::Cluster::LifecycleEvents).to receive(:on_worker_stop) # stub this out to not mutate global state
expect_next_instance_of(Gitlab::Memory::ReportsDaemon) do |daemon|
- expect(daemon).to receive(:start).and_call_original
+ expect(daemon).to receive(:start)
+ end
- # make sleep no-op
- allow(daemon).to receive(:sleep).and_return(nil)
+ load_initializer
+ end
- # let alive return 3 times: true, true, false
- allow(daemon).to receive(:alive).and_return(true, true, false)
- end
+ it 'writes scheduled heap dumps in on_worker_stop' do
+ expect(Gitlab::Cluster::LifecycleEvents).to receive(:on_worker_start)
+ expect(Gitlab::Cluster::LifecycleEvents).to receive(:on_worker_stop).and_call_original
+ expect(Gitlab::Memory::Reporter).to receive(:new).and_return(reporter)
+ expect(reporter).to receive(:run_report).with(an_instance_of(Gitlab::Memory::Reports::HeapDump))
load_initializer
+ # This is necessary because this hook normally fires during worker shutdown.
+ Gitlab::Cluster::LifecycleEvents.do_worker_stop
end
end
@@ -50,7 +58,7 @@ RSpec.describe 'diagnostic reports' do
allow(::Gitlab::Runtime).to receive(:puma?).and_return(false)
end
- include_examples 'does not modify worker startup hooks'
+ include_examples 'does not modify worker hooks'
end
end
@@ -59,7 +67,7 @@ RSpec.describe 'diagnostic reports' do
allow(::Gitlab::Runtime).to receive(:puma?).and_return(true)
end
- include_examples 'does not modify worker startup hooks'
+ include_examples 'does not modify worker hooks'
end
context 'when GITLAB_DIAGNOSTIC_REPORTS_ENABLED is set to false' do
@@ -68,6 +76,6 @@ RSpec.describe 'diagnostic reports' do
allow(::Gitlab::Runtime).to receive(:puma?).and_return(true)
end
- include_examples 'does not modify worker startup hooks'
+ include_examples 'does not modify worker hooks'
end
end
diff --git a/spec/initializers/forbid_sidekiq_in_transactions_spec.rb b/spec/initializers/forbid_sidekiq_in_transactions_spec.rb
index a89ac73f6fa..7b1907a7451 100644
--- a/spec/initializers/forbid_sidekiq_in_transactions_spec.rb
+++ b/spec/initializers/forbid_sidekiq_in_transactions_spec.rb
@@ -34,8 +34,7 @@ RSpec.describe 'Sidekiq::Worker' do
Class.new do
include Sidekiq::Worker
- def perform
- end
+ def perform; end
end
end
@@ -47,8 +46,7 @@ RSpec.describe 'Sidekiq::Worker' do
context 'for mailers' do
let(:mailer_class) do
Class.new(ApplicationMailer) do
- def test_mail
- end
+ def test_mail; end
end
end
diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb
index 0a794e8ebcd..c423c144dc2 100644
--- a/spec/initializers/lograge_spec.rb
+++ b/spec/initializers/lograge_spec.rb
@@ -93,7 +93,7 @@ RSpec.describe 'lograge', type: :request do
include MemoryInstrumentationHelper
before do
- skip_memory_instrumentation!
+ verify_memory_instrumentation_available!
end
it 'logs memory usage metrics' do
diff --git a/spec/initializers/rails_yaml_safe_load_spec.rb b/spec/initializers/rails_yaml_safe_load_spec.rb
index 8cf6a3676e0..714c568b07a 100644
--- a/spec/initializers/rails_yaml_safe_load_spec.rb
+++ b/spec/initializers/rails_yaml_safe_load_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Rails YAML safe load' do
+ let_it_be(:project_namespace) { create(:project_namespace) }
+
let(:unsafe_load) { false }
let(:klass) do
@@ -13,7 +15,8 @@ RSpec.describe 'Rails YAML safe load' do
end
end
- let(:instance) { klass.new(description: data) }
+ let(:issue_type) { WorkItems::Type.default_by_type(:issue) }
+ let(:instance) { klass.new(description: data, work_item_type_id: issue_type.id, namespace_id: project_namespace.id) }
context 'with default permitted classes' do
let(:data) do
diff --git a/spec/lib/api/ci/helpers/runner_helpers_spec.rb b/spec/lib/api/ci/helpers/runner_helpers_spec.rb
index b254c419cbc..d32f7e4f0be 100644
--- a/spec/lib/api/ci/helpers/runner_helpers_spec.rb
+++ b/spec/lib/api/ci/helpers/runner_helpers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Helpers::Runner do
+RSpec.describe API::Ci::Helpers::Runner, feature_category: :runner do
let(:ip_address) { '1.2.3.4' }
let(:runner_class) do
Class.new do
diff --git a/spec/lib/api/entities/package_spec.rb b/spec/lib/api/entities/package_spec.rb
index d63ea7833ac..9288f6fe8eb 100644
--- a/spec/lib/api/entities/package_spec.rb
+++ b/spec/lib/api/entities/package_spec.rb
@@ -32,4 +32,12 @@ RSpec.describe API::Entities::Package do
expect(subject[:_links][:web_path]).to match('/infrastructure_registry/')
end
end
+
+ context 'when package has no default status' do
+ let(:package) { create(:package, :error) }
+
+ it 'does not expose web_path in _links' do
+ expect(subject[:_links]).not_to have_key(:web_path)
+ end
+ end
end
diff --git a/spec/lib/api/entities/plan_limit_spec.rb b/spec/lib/api/entities/plan_limit_spec.rb
index a88ea3f4cad..baaaeb0b600 100644
--- a/spec/lib/api/entities/plan_limit_spec.rb
+++ b/spec/lib/api/entities/plan_limit_spec.rb
@@ -25,7 +25,8 @@ RSpec.describe API::Entities::PlanLimit do
:nuget_max_file_size,
:pypi_max_file_size,
:terraform_module_max_file_size,
- :storage_size_limit
+ :storage_size_limit,
+ :pipeline_hierarchy_size
)
end
diff --git a/spec/lib/api/entities/ssh_key_spec.rb b/spec/lib/api/entities/ssh_key_spec.rb
index 768ad416fbe..b4310035a66 100644
--- a/spec/lib/api/entities/ssh_key_spec.rb
+++ b/spec/lib/api/entities/ssh_key_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Entities::SSHKey do
+RSpec.describe API::Entities::SSHKey, feature_category: :authentication_and_authorization do
describe '#as_json' do
subject { entity.as_json }
@@ -15,7 +15,8 @@ RSpec.describe API::Entities::SSHKey do
title: key.title,
created_at: key.created_at,
expires_at: key.expires_at,
- key: key.publishable_key
+ key: key.publishable_key,
+ usage_type: 'auth_and_signing'
)
end
end
diff --git a/spec/lib/api/every_api_endpoint_spec.rb b/spec/lib/api/every_api_endpoint_spec.rb
index 5fe14823a29..c45ff9eb628 100644
--- a/spec/lib/api/every_api_endpoint_spec.rb
+++ b/spec/lib/api/every_api_endpoint_spec.rb
@@ -32,10 +32,21 @@ RSpec.describe 'Every API endpoint' do
next unless used_category
next if used_category == :not_owned
- [path, used_category] unless feature_categories.include?(used_category)
+ [klass, path, used_category] unless feature_categories.include?(used_category)
end.compact
- expect(routes_unknown_category).to be_empty, "#{routes_unknown_category.first(10)} had an unknown category"
+ message = -> do
+ list = routes_unknown_category.map do |klass, path, category|
+ "- #{klass} (#{path}): #{category}"
+ end
+
+ <<~MESSAGE
+ Unknown categories found for:
+ #{list.join("\n")}
+ MESSAGE
+ end
+
+ expect(routes_unknown_category).to be_empty, message
end
# This is required for API::Base.path_for_app to work, as it picks
diff --git a/spec/lib/api/helpers/packages_helpers_spec.rb b/spec/lib/api/helpers/packages_helpers_spec.rb
index b9c887b3e16..a3b21059334 100644
--- a/spec/lib/api/helpers/packages_helpers_spec.rb
+++ b/spec/lib/api/helpers/packages_helpers_spec.rb
@@ -238,4 +238,26 @@ RSpec.describe API::Helpers::PackagesHelpers do
end
end
end
+
+ describe '#track_package_event' do
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:action) { 'push_package' }
+ let(:scope) { :terraform_module }
+ let(:category) { described_class.name }
+ let(:namespace) { project.namespace }
+ let(:user) { project.creator }
+ let(:feature_flag_name) { nil }
+ let(:label) { 'redis_hll_counters.user_packages.user_packages_total_unique_counts_monthly' }
+ let(:property) { 'i_package_terraform_module_user' }
+
+ subject(:package_action) do
+ args = { category: category, namespace: namespace, user: user, project: project }
+ helper.track_package_event(action, scope, **args)
+ 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 2fed1cf3604..3640c7e30e7 100644
--- a/spec/lib/api/helpers/rate_limiter_spec.rb
+++ b/spec/lib/api/helpers/rate_limiter_spec.rb
@@ -19,8 +19,7 @@ RSpec.describe API::Helpers::RateLimiter do
@current_user = current_user
end
- def render_api_error!(**args)
- end
+ def render_api_error!(**args); end
end
end
diff --git a/spec/lib/api/support/git_access_actor_spec.rb b/spec/lib/api/support/git_access_actor_spec.rb
index e1c800d25a7..b3e8787583c 100644
--- a/spec/lib/api/support/git_access_actor_spec.rb
+++ b/spec/lib/api/support/git_access_actor_spec.rb
@@ -9,7 +9,8 @@ RSpec.describe API::Support::GitAccessActor do
subject { described_class.new(user: user, key: key) }
describe '.from_params' do
- let(:key) { create(:key) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:key) { create(:key, user: user) }
context 'with params that are valid' do
it 'returns an instance of API::Support::GitAccessActor' do
@@ -31,6 +32,42 @@ RSpec.describe API::Support::GitAccessActor do
expect(described_class.from_params(identifier: "key-#{key.id}").user).to eq(key.user)
end
end
+
+ context 'when passing a signing key' do
+ let_it_be(:key) { create(:key, usage_type: :signing, user: user) }
+
+ it 'does not identify the user' do
+ actor = described_class.from_params({ identifier: "key-#{key.id}" })
+
+ expect(actor).to be_instance_of(described_class)
+ expect(actor.user).to be_nil
+ end
+
+ it 'does not identify the key' do
+ actor = described_class.from_params({ key_id: key.id })
+
+ expect(actor).to be_instance_of(described_class)
+ expect(actor.key).to be_nil
+ end
+ end
+
+ context 'when passing an auth-only key' do
+ let_it_be(:key) { create(:key, usage_type: :auth, user: user) }
+
+ it 'identifies the user' do
+ actor = described_class.from_params({ identifier: "key-#{key.id}" })
+
+ expect(actor).to be_instance_of(described_class)
+ expect(actor.user).to eq(key.user)
+ end
+
+ it 'identifies the key' do
+ actor = described_class.from_params({ key_id: key.id })
+
+ expect(actor).to be_instance_of(described_class)
+ expect(actor.key).to eq(key)
+ end
+ end
end
describe 'attributes' do
diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb
index 0ae0f02c46e..a8ee28d3714 100644
--- a/spec/lib/atlassian/jira_connect/client_spec.rb
+++ b/spec/lib/atlassian/jira_connect/client_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Atlassian::JiraConnect::Client do
+RSpec.describe Atlassian::JiraConnect::Client, feature_category: :integrations do
include StubRequests
subject(:client) { described_class.new('https://gitlab-test.atlassian.net', 'sample_secret') }
@@ -119,7 +119,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
let(:errors) { [{ 'message' => 'X' }, { 'message' => 'Y' }] }
let(:processed) { subject.send(:handle_response, response, 'foo') { |x| [:data, x] } }
- context 'the response is 200 OK' do
+ context 'when the response is 200 OK' do
let(:response) { double(code: 200, parsed_response: :foo) }
it 'yields to the block' do
@@ -127,7 +127,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
- context 'the response is 202 accepted' do
+ context 'when the response is 202 accepted' do
let(:response) { double(code: 202, parsed_response: :foo) }
it 'yields to the block' do
@@ -135,15 +135,15 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
- context 'the response is 400 bad request' do
+ context 'when the response is 400 bad request' do
let(:response) { double(code: 400, parsed_response: errors) }
it 'extracts the errors messages' do
- expect(processed).to eq('errorMessages' => %w(X Y), 'responseCode' => 400)
+ expect(processed).to eq('errorMessages' => %w[X Y], 'responseCode' => 400)
end
end
- context 'the response is 401 forbidden' do
+ context 'when the response is 401 forbidden' do
let(:response) { double(code: 401, parsed_response: nil) }
it 'reports that our JWT is wrong' do
@@ -151,7 +151,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
- context 'the response is 403' do
+ context 'when the response is 403' do
let(:response) { double(code: 403, parsed_response: nil) }
it 'reports that the App is misconfigured' do
@@ -159,7 +159,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
- context 'the response is 413' do
+ context 'when the response is 413' do
let(:response) { double(code: 413, parsed_response: errors) }
it 'extracts the errors messages' do
@@ -167,7 +167,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
- context 'the response is 429' do
+ context 'when the response is 429' do
let(:response) { double(code: 429, parsed_response: nil) }
it 'reports that we exceeded the rate limit' do
@@ -175,7 +175,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
- context 'the response is 503' do
+ context 'when the response is 503' do
let(:response) { double(code: 503, parsed_response: nil) }
it 'reports that the service is unavailable' do
@@ -183,7 +183,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
- context 'the response is anything else' do
+ context 'when the response is anything else' do
let(:response) { double(code: 1000, parsed_response: :something) }
it 'reports that this was unanticipated' do
@@ -192,6 +192,26 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
+ describe '#request_body_schema' do
+ let(:response) { instance_double(HTTParty::Response, success?: true, code: 200, request: request) }
+
+ context 'with valid JSON request body' do
+ let(:request) { instance_double(HTTParty::Request, raw_body: '{ "foo": 1, "bar": 2 }') }
+
+ it 'returns the request body' do
+ expect(subject.send(:request_body_schema, response)).to eq({ "foo" => nil, "bar" => nil })
+ end
+ end
+
+ context 'with invalid JSON request body' do
+ let(:request) { instance_double(HTTParty::Request, raw_body: 'invalid json') }
+
+ it 'reports the invalid json' do
+ expect(subject.send(:request_body_schema, response)).to eq('Request body includes invalid JSON')
+ end
+ end
+ end
+
describe '#store_deploy_info' do
let_it_be(:environment) { create(:environment, name: 'DEV', project: project) }
let_it_be(:deployments) do
@@ -222,7 +242,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
before do
path = '/rest/deployments/0.1/bulk'
- stub_full_request('https://gitlab-test.atlassian.net' + path, method: :post)
+ stub_full_request("https://gitlab-test.atlassian.net#{path}", method: :post)
.with(body: body, headers: expected_headers(path))
.to_return(body: response_body, headers: { 'Content-Type': 'application/json' })
end
@@ -232,7 +252,9 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
it 'only sends information about relevant MRs' do
- expect(subject).to receive(:post).with('/rest/deployments/0.1/bulk', { deployments: have_attributes(size: 6) }).and_call_original
+ expect(subject).to receive(:post).with(
+ '/rest/deployments/0.1/bulk', { deployments: have_attributes(size: 6) }
+ ).and_call_original
subject.send(:store_deploy_info, project: project, deployments: deployments)
end
@@ -243,7 +265,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
subject.send(:store_deploy_info, project: project, deployments: deployments.take(1))
end
- context 'there are errors' do
+ context 'when there are errors' do
let(:rejections) do
[{ errors: [{ message: 'X' }, { message: 'Y' }] }, { errors: [{ message: 'Z' }] }]
end
@@ -251,7 +273,9 @@ RSpec.describe Atlassian::JiraConnect::Client do
it 'reports the errors' do
response = subject.send(:store_deploy_info, project: project, deployments: deployments)
- expect(response['errorMessages']).to eq(%w(X Y Z))
+ expect(response['errorMessages']).to eq(%w[X Y Z])
+ expect(response['responseCode']).to eq(200)
+ expect(response['requestBody']).to be_a(Hash)
end
end
end
@@ -282,7 +306,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
feature_flags.first.update!(description: 'RELEVANT-123')
feature_flags.second.update!(description: 'RELEVANT-123')
path = '/rest/featureflags/0.1/bulk'
- stub_full_request('https://gitlab-test.atlassian.net' + path, method: :post)
+ stub_full_request("https://gitlab-test.atlassian.net#{path}", method: :post)
.with(body: body, headers: expected_headers(path))
.to_return(body: response_body, headers: { 'Content-Type': 'application/json' })
end
@@ -292,9 +316,9 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
it 'only sends information about relevant MRs' do
- expect(subject).to receive(:post).with('/rest/featureflags/0.1/bulk', {
- flags: have_attributes(size: 2), properties: Hash
- }).and_call_original
+ expect(subject).to receive(:post).with(
+ '/rest/featureflags/0.1/bulk', { flags: have_attributes(size: 2), properties: Hash }
+ ).and_call_original
subject.send(:store_ff_info, project: project, feature_flags: feature_flags)
end
@@ -305,7 +329,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
subject.send(:store_ff_info, project: project, feature_flags: [feature_flags.last])
end
- context 'there are errors' do
+ context 'when there are errors' do
let(:failures) do
{
a: [{ message: 'X' }, { message: 'Y' }],
@@ -343,7 +367,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
before do
path = '/rest/builds/0.1/bulk'
- stub_full_request('https://gitlab-test.atlassian.net' + path, method: :post)
+ stub_full_request("https://gitlab-test.atlassian.net#{path}", method: :post)
.with(body: body, headers: expected_headers(path))
.to_return(body: response_body, headers: { 'Content-Type': 'application/json' })
end
@@ -366,7 +390,7 @@ RSpec.describe Atlassian::JiraConnect::Client do
subject.send(:store_build_info, project: project, pipelines: pipelines.take(1))
end
- context 'there are errors' do
+ context 'when there are errors' do
let(:failures) do
[{ errors: [{ message: 'X' }, { message: 'Y' }] }, { errors: [{ message: 'Z' }] }]
end
@@ -374,7 +398,9 @@ RSpec.describe Atlassian::JiraConnect::Client do
it 'reports the errors' do
response = subject.send(:store_build_info, project: project, pipelines: pipelines)
- expect(response['errorMessages']).to eq(%w(X Y Z))
+ expect(response['errorMessages']).to eq(%w[X Y Z])
+ expect(response['responseCode']).to eq(200)
+ expect(response['requestBody']).to be_a(Hash)
end
end
@@ -385,19 +411,21 @@ RSpec.describe Atlassian::JiraConnect::Client do
subject.send(:store_build_info, project: project, pipelines: pipelines)
end
- pipelines << create(:ci_pipeline, head_pipeline_of: create(:merge_request, :jira_branch))
+ pipelines << create(:ci_pipeline, project: project, head_pipeline_of: create(:merge_request, :jira_branch, source_project: project))
- expect { subject.send(:store_build_info, project: project, pipelines: pipelines) }.not_to exceed_query_limit(baseline)
+ expect do
+ subject.send(:store_build_info, project: project, pipelines: pipelines)
+ end.not_to exceed_query_limit(baseline)
end
end
describe '#store_dev_info' do
- let_it_be(:merge_requests) { create_list(:merge_request, 2, :unique_branches) }
+ let_it_be(:merge_requests) { create_list(:merge_request, 2, :unique_branches, source_project: project) }
before do
path = '/rest/devinfo/0.10/bulk'
- stub_full_request('https://gitlab-test.atlassian.net' + path, method: :post)
+ stub_full_request("https://gitlab-test.atlassian.net#{path}", method: :post)
.with(headers: expected_headers(path))
end
@@ -406,11 +434,16 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
it 'avoids N+1 database queries' do
- control_count = ActiveRecord::QueryRecorder.new { subject.send(:store_dev_info, project: project, merge_requests: merge_requests) }.count
+ control_count = ActiveRecord::QueryRecorder.new do
+ subject.send(:store_dev_info, project: project, merge_requests: merge_requests)
+ end.count
- merge_requests << create(:merge_request, :unique_branches)
+ merge_requests << create(:merge_request, :unique_branches, source_project: project)
- expect { subject.send(:store_dev_info, project: project, merge_requests: merge_requests) }.not_to exceed_query_limit(control_count)
+ expect do
+ subject.send(:store_dev_info, project: project,
+ merge_requests: merge_requests)
+ end.not_to exceed_query_limit(control_count)
end
end
diff --git a/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb b/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb
index 86d672067a3..89c85489aea 100644
--- a/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb
+++ b/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do
+RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric, feature_category: :integrations do
describe '#valid?' do
let_it_be(:private_key) { OpenSSL::PKey::RSA.generate 3072 }
@@ -17,6 +17,7 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do
let(:jwt) { JWT.encode(jwt_claims, private_key, 'RS256', jwt_headers) }
let(:public_key) { private_key.public_key }
let(:stub_asymmetric_jwt_cdn) { 'https://connect-install-keys.atlassian.com' }
+ let(:jira_connect_proxy_url_setting) { nil }
let(:install_keys_url) { "#{stub_asymmetric_jwt_cdn}/#{public_key_id}" }
let(:qsh) do
Atlassian::Jwt.create_query_string_hash('https://gitlab.test/events/installed', 'POST', 'https://gitlab.test')
@@ -25,6 +26,8 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do
before do
stub_request(:get, install_keys_url)
.to_return(body: public_key.to_s, status: 200)
+
+ stub_application_setting(jira_connect_proxy_url: jira_connect_proxy_url_setting)
end
it 'returns true when verified with public key from CDN' do
@@ -89,10 +92,7 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do
context 'with jira_connect_proxy_url setting' do
let(:stub_asymmetric_jwt_cdn) { 'https://example.com/-/jira_connect/public_keys' }
-
- before do
- stub_application_setting(jira_connect_proxy_url: 'https://example.com')
- end
+ let(:jira_connect_proxy_url_setting) { 'https://example.com' }
it 'requests the settings CDN' do
expect(JWT).to receive(:decode).twice.and_call_original
@@ -101,22 +101,6 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do
expect(WebMock).to have_requested(:get, "https://example.com/-/jira_connect/public_keys/#{public_key_id}")
end
-
- context 'when jira_connect_oauth_self_managed disabled' do
- let(:stub_asymmetric_jwt_cdn) { 'https://connect-install-keys.atlassian.com' }
-
- before do
- stub_feature_flags(jira_connect_oauth_self_managed: false)
- end
-
- it 'requests the default CDN' do
- expect(JWT).to receive(:decode).twice.and_call_original
-
- expect(asymmetric_jwt).to be_valid
-
- expect(WebMock).to have_requested(:get, install_keys_url)
- end
- end
end
end
diff --git a/spec/lib/atlassian/jira_connect/jwt/symmetric_spec.rb b/spec/lib/atlassian/jira_connect/jwt/symmetric_spec.rb
index 61adff7e221..109868f5d95 100644
--- a/spec/lib/atlassian/jira_connect/jwt/symmetric_spec.rb
+++ b/spec/lib/atlassian/jira_connect/jwt/symmetric_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Atlassian::JiraConnect::Jwt::Symmetric do
+RSpec.describe Atlassian::JiraConnect::Jwt::Symmetric, feature_category: :integrations do
let(:shared_secret) { 'secret' }
describe '#iss_claim' do
diff --git a/spec/lib/atlassian/jira_connect/serializers/author_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/author_entity_spec.rb
index f31cf929244..e1158bb5988 100644
--- a/spec/lib/atlassian/jira_connect/serializers/author_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/author_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Atlassian::JiraConnect::Serializers::AuthorEntity do
+RSpec.describe Atlassian::JiraConnect::Serializers::AuthorEntity, feature_category: :integrations do
subject { described_class.represent(user).as_json }
context 'when object is a User model' do
diff --git a/spec/lib/atlassian/jira_connect/serializers/base_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/base_entity_spec.rb
index d7672c0baf1..f34bec1413b 100644
--- a/spec/lib/atlassian/jira_connect/serializers/base_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/base_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Atlassian::JiraConnect::Serializers::BaseEntity do
+RSpec.describe Atlassian::JiraConnect::Serializers::BaseEntity, feature_category: :integrations do
let(:update_sequence_id) { nil }
subject do
diff --git a/spec/lib/atlassian/jira_connect/serializers/branch_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/branch_entity_spec.rb
index e69e2aae94c..86e48a4a0fd 100644
--- a/spec/lib/atlassian/jira_connect/serializers/branch_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/branch_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Atlassian::JiraConnect::Serializers::BranchEntity do
+RSpec.describe Atlassian::JiraConnect::Serializers::BranchEntity, feature_category: :integrations do
let(:project) { create(:project, :repository) }
let(:branch) { project.repository.find_branch('improve/awesome') }
diff --git a/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb
index a29f32d35b8..48787f2a0d2 100644
--- a/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Atlassian::JiraConnect::Serializers::BuildEntity do
+RSpec.describe Atlassian::JiraConnect::Serializers::BuildEntity, feature_category: :integrations do
let_it_be(:user) { create_default(:user) }
let_it_be(:project) { create_default(:project) }
diff --git a/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb
index 40b9e83719b..f6fca39fa68 100644
--- a/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity do
+RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_category: :integrations do
let_it_be(:user) { create_default(:user) }
let_it_be(:project) { create_default(:project, :repository) }
let_it_be(:environment) { create(:environment, name: 'prod', project: project) }
diff --git a/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb
index 2d12cd1ed0a..3f84404f38d 100644
--- a/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity do
+RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity, feature_category: :integrations do
let_it_be(:user) { create_default(:user) }
let_it_be(:project) { create_default(:project) }
diff --git a/spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb
index 6399fc9053b..5ebb5ffed3b 100644
--- a/spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Atlassian::JiraConnect::Serializers::PullRequestEntity do
+RSpec.describe Atlassian::JiraConnect::Serializers::PullRequestEntity, feature_category: :integrations do
let_it_be(:project) { create_default(:project, :repository) }
let_it_be(:merge_requests) { create_list(:merge_request, 2, :unique_branches) }
let_it_be(:notes) { create_list(:note, 2, system: false, noteable: merge_requests.first) }
diff --git a/spec/lib/atlassian/jira_connect/serializers/repository_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/repository_entity_spec.rb
index 9100398ecc5..2a4fba0f00e 100644
--- a/spec/lib/atlassian/jira_connect/serializers/repository_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/repository_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Atlassian::JiraConnect::Serializers::RepositoryEntity do
+RSpec.describe Atlassian::JiraConnect::Serializers::RepositoryEntity, feature_category: :integrations do
let(:update_sequence_id) { nil }
subject do
diff --git a/spec/lib/atlassian/jira_connect_spec.rb b/spec/lib/atlassian/jira_connect_spec.rb
index d9c34e938b4..14bf13b8fe6 100644
--- a/spec/lib/atlassian/jira_connect_spec.rb
+++ b/spec/lib/atlassian/jira_connect_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Atlassian::JiraConnect do
+RSpec.describe Atlassian::JiraConnect, feature_category: :integrations do
describe '.app_name' do
subject { described_class.app_name }
@@ -25,5 +25,13 @@ RSpec.describe Atlassian::JiraConnect do
expect(app_key).to eq('gitlab-jira-connect-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
end
end
+
+ context 'with jira_connect_proxy_url setting' do
+ before do
+ stub_application_setting(jira_connect_proxy_url: 'https://example.com')
+ end
+
+ it { is_expected.to eq('gitlab-jira-connect-example.com') }
+ end
end
end
diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb
index 6b0747735ed..7cc8ce2cbae 100644
--- a/spec/lib/backup/gitaly_backup_spec.rb
+++ b/spec/lib/backup/gitaly_backup_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe Backup::GitalyBackup do
it 'erases any existing repository backups' do
existing_file = File.join(destination, 'some_existing_file')
- IO.write(existing_file, "Some existing file.\n")
+ File.write(existing_file, "Some existing file.\n")
subject.start(:create, destination, backup_id: backup_id)
subject.finish!
diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index f85b005f4d1..992dbec73c2 100644
--- a/spec/lib/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
@@ -166,7 +166,7 @@ RSpec.describe Backup::Manager do
describe '#create' do
let(:incremental_env) { 'false' }
let(:expected_backup_contents) { %w{backup_information.yml task1.tar.gz task2.tar.gz} }
- let(:backup_time) { Time.utc(2019, 1, 1) }
+ let(:backup_time) { Time.zone.parse('2019-1-1') }
let(:backup_id) { "1546300800_2019_01_01_#{Gitlab::VERSION}" }
let(:full_backup_id) { backup_id }
let(:pack_tar_file) { "#{backup_id}_gitlab_backup.tar" }
@@ -284,7 +284,7 @@ RSpec.describe Backup::Manager do
allow(Dir).to receive(:chdir).and_yield
allow(Dir).to receive(:glob).and_return(files)
allow(FileUtils).to receive(:rm)
- allow(Time).to receive(:now).and_return(Time.utc(2016))
+ allow(Time).to receive(:now).and_return(Time.zone.parse('2016-1-1'))
end
context 'when keep_time is zero' do
diff --git a/spec/lib/banzai/filter/attributes_filter_spec.rb b/spec/lib/banzai/filter/attributes_filter_spec.rb
new file mode 100644
index 00000000000..cef5e24cdaa
--- /dev/null
+++ b/spec/lib/banzai/filter/attributes_filter_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::AttributesFilter, feature_category: :team_planning do
+ using RSpec::Parameterized::TableSyntax
+ include FilterSpecHelper
+
+ def image
+ %(<img src="example.jpg">)
+ end
+
+ describe 'attribute syntax' do
+ context 'when attribute syntax is valid' do
+ where(:text, :result) do
+ "#{image}{width=100}" | '<img src="example.jpg" width="100">'
+ "#{image}{ width=100 }" | '<img src="example.jpg" width="100">'
+ "#{image}{width=\"100\"}" | '<img src="example.jpg" width="100">'
+ "#{image}{width=100 width=200}" | '<img src="example.jpg" width="200">'
+
+ "#{image}{.test_class width=100 style=\"width:400\"}" | '<img src="example.jpg" width="100">'
+ "<img src=\"example.jpg\" class=\"lazy\" />{width=100}" | '<img src="example.jpg" class="lazy" width="100">'
+ end
+
+ with_them do
+ it 'adds them to the img' do
+ expect(filter(text).to_html).to eq result
+ end
+ end
+ end
+
+ context 'when attribute syntax is invalid' do
+ where(:text, :result) do
+ "#{image} {width=100}" | '<img src="example.jpg"> {width=100}'
+ "#{image}{width=100\nheight=100}" | "<img src=\"example.jpg\">{width=100\nheight=100}"
+ "{width=100 height=100}\n#{image}" | "{width=100 height=100}\n<img src=\"example.jpg\">"
+ '<h1>header</h1>{width=100}' | '<h1>header</h1>{width=100}'
+ end
+
+ with_them do
+ it 'does not recognize as attributes' do
+ expect(filter(text).to_html).to eq result
+ end
+ end
+ end
+ end
+
+ describe 'height and width' do
+ context 'when size attributes are valid' do
+ where(:text, :result) do
+ "#{image}{width=100 height=200px}" | '<img src="example.jpg" width="100" height="200px">'
+ "#{image}{width=100}" | '<img src="example.jpg" width="100">'
+ "#{image}{width=100px}" | '<img src="example.jpg" width="100px">'
+ "#{image}{height=100%}" | '<img src="example.jpg" height="100%">'
+ "#{image}{width=\"100%\"}" | '<img src="example.jpg" width="100%">'
+ end
+
+ with_them do
+ it 'adds them to the img' do
+ expect(filter(text).to_html).to eq result
+ end
+ end
+ end
+
+ context 'when size attributes are invalid' do
+ where(:text, :result) do
+ "#{image}{width=100cs}" | '<img src="example.jpg">'
+ "#{image}{width=auto height=200}" | '<img src="example.jpg" height="200">'
+ end
+
+ with_them do
+ it 'ignores them' do
+ expect(filter(text).to_html).to eq result
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
index c22517621c1..3ebe0798972 100644
--- a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'ffaker'
-RSpec.describe Banzai::Filter::CommitTrailersFilter do
+RSpec.describe Banzai::Filter::CommitTrailersFilter, feature_category: :source_code_management do
include FilterSpecHelper
include CommitTrailersSpecHelper
diff --git a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
index d29af311ee5..db0c10a802b 100644
--- a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
@@ -29,12 +29,12 @@ RSpec.describe Banzai::Filter::InlineGrafanaMetricsFilter do
)
end
- it_behaves_like 'a metrics embed filter'
-
around do |example|
travel_to(Time.utc(2019, 3, 17, 13, 10)) { example.run }
end
+ it_behaves_like 'a metrics embed filter'
+
context 'when grafana is not configured' do
before do
allow(project).to receive(:grafana_integration).and_return(nil)
diff --git a/spec/lib/banzai/filter/inline_observability_filter_spec.rb b/spec/lib/banzai/filter/inline_observability_filter_spec.rb
new file mode 100644
index 00000000000..341ada6d2b5
--- /dev/null
+++ b/spec/lib/banzai/filter/inline_observability_filter_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::InlineObservabilityFilter do
+ include FilterSpecHelper
+
+ let(:input) { %(<a href="#{url}">example</a>) }
+ let(:doc) { filter(input) }
+
+ context 'when the document has an external link' do
+ let(:url) { 'https://foo.com' }
+
+ it 'leaves regular non-observability links unchanged' do
+ expect(doc.to_s).to eq(input)
+ end
+ end
+
+ context 'when the document contains an embeddable observability link' do
+ let(:url) { 'https://observe.gitlab.com/12345' }
+
+ it 'leaves the original link unchanged' do
+ expect(doc.at_css('a').to_s).to eq(input)
+ end
+
+ it 'appends a observability charts placeholder' do
+ node = doc.at_css('.js-render-observability')
+
+ expect(node).to be_present
+ expect(node.attribute('data-frame-url').to_s).to eq(url)
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/references/reference_filter_spec.rb b/spec/lib/banzai/filter/references/reference_filter_spec.rb
index b14b9374364..6d7396ef216 100644
--- a/spec/lib/banzai/filter/references/reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/reference_filter_spec.rb
@@ -148,7 +148,7 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do
include_context 'document nodes'
let(:node) { Nokogiri::HTML.fragment('text @reference') }
- let(:ref_pattern) { %r{(?<!\w)@(?<user>[a-zA-Z0-9_\-\.]*)}x }
+ let(:ref_pattern) { %r{(?<!\w)@(?<user>[a-zA-Z0-9_\-.]*)}x }
context 'when node has no reference pattern' do
let(:node) { Nokogiri::HTML.fragment('random text') }
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 b153efd9655..d61b71c711d 100644
--- a/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
@@ -42,12 +42,12 @@ RSpec.describe Banzai::Filter::References::UserReferenceFilter do
context 'mentioning @all' do
let(:reference) { User.reference_prefix + 'all' }
- it_behaves_like 'a reference containing an element node'
-
before do
project.add_developer(project.creator)
end
+ it_behaves_like 'a reference containing an element node'
+
it 'supports a special @all mention' do
project.add_developer(user)
doc = reference_filter("Hey #{reference}", author: user)
diff --git a/spec/lib/banzai/filter/repository_link_filter_spec.rb b/spec/lib/banzai/filter/repository_link_filter_spec.rb
index 4aeb6e2a722..0df680dc0c8 100644
--- a/spec/lib/banzai/filter/repository_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/repository_link_filter_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe Banzai::Filter::RepositoryLinkFilter do
expect { filter(raw_doc) }.to change { Gitlab::GitalyClient.get_request_count }.by(2)
end
- shared_examples :preserve_unchanged do
+ shared_examples 'preserve unchanged' do
it 'does not modify any relative URL in anchor' do
doc = filter(link('README.md'))
expect(doc.at_css('a')['href']).to eq 'README.md'
@@ -96,25 +96,25 @@ RSpec.describe Banzai::Filter::RepositoryLinkFilter do
context 'with a wiki' do
let(:wiki) { double('ProjectWiki') }
- include_examples :preserve_unchanged
+ include_examples 'preserve unchanged'
end
context 'without a repository' do
let(:project) { create(:project) }
- include_examples :preserve_unchanged
+ include_examples 'preserve unchanged'
end
context 'with an empty repository' do
let(:project) { create(:project_empty_repo) }
- include_examples :preserve_unchanged
+ include_examples 'preserve unchanged'
end
context 'without project repository access' do
let(:project) { create(:project, :repository, repository_access_level: ProjectFeature::PRIVATE) }
- include_examples :preserve_unchanged
+ include_examples 'preserve unchanged'
end
it 'does not raise an exception on invalid URIs' do
@@ -147,7 +147,7 @@ RSpec.describe Banzai::Filter::RepositoryLinkFilter do
.to eq "/#{project_path}/-/blob/#{ref}/non/existent.file"
end
- shared_examples :valid_repository do
+ shared_examples 'valid repository' do
it 'handles Gitaly unavailable exceptions gracefully' do
allow_next_instance_of(Gitlab::GitalyClient::BlobService) do |blob_service|
allow(blob_service).to receive(:get_blob_types).and_raise(GRPC::Unavailable)
@@ -364,13 +364,13 @@ RSpec.describe Banzai::Filter::RepositoryLinkFilter do
end
context 'with a valid commit' do
- include_examples :valid_repository
+ include_examples 'valid repository'
end
context 'with a valid ref' do
# force filter to use ref instead of commit
let(:commit) { nil }
- include_examples :valid_repository
+ include_examples 'valid repository'
end
end
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index a409c15533b..b4be26ef8d2 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
result = filter(%{<pre lang="#{lang}"><code>&lt;script&gt;alert(1)&lt;/script&gt;</code></pre>})
# `(1)` symbols are wrapped by lexer tags.
- expect(result.to_html).not_to match(%r{<script>alert.*<\/script>})
+ expect(result.to_html).not_to match(%r{<script>alert.*</script>})
# `<>` stands for lexer tags like <span ...>, not &lt;s above.
expect(result.to_html).to match(%r{alert(<.*>)?\((<.*>)?1(<.*>)?\)})
@@ -192,4 +192,8 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
include_examples "XSS prevention", "ruby"
end
+
+ it_behaves_like "filter timeout" do
+ let(:text) { '<pre lang="ruby"><code>def fun end</code></pre>' }
+ end
end
diff --git a/spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb b/spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb
new file mode 100644
index 00000000000..95d2e54459d
--- /dev/null
+++ b/spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::TimeoutHtmlPipelineFilter do
+ include FilterSpecHelper
+
+ it_behaves_like 'filter timeout' do
+ let(:text) { '<p>some text</p>' }
+ end
+
+ it 'raises NotImplementedError' do
+ expect { filter('test') }.to raise_error NotImplementedError
+ end
+end
diff --git a/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb b/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb
index 4bccae04fda..8d15dbc8f2f 100644
--- a/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb
@@ -78,7 +78,7 @@ RSpec.describe Banzai::Pipeline::IncidentManagement::TimelineEventPipeline do
it 'replaces existing label to a link' do
# rubocop:disable Layout/LineLength
is_expected.to match(
- %r(<p>.+<a href=\"[\w/]+-/issues\?label_name=#{label.name}\".+style=\"background-color: #\d{6}\".*>#{label.name}</span></a></span> ~unknown</p>)
+ %r(<p>.+<a href="[\w/]+-/issues\?label_name=#{label.name}".+style="background-color: #\d{6}".*>#{label.name}</span></a></span> ~unknown</p>)
)
# rubocop:enable Layout/LineLength
end
diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb
index 9e77137795a..61751b69842 100644
--- a/spec/lib/banzai/reference_parser/base_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb
@@ -18,6 +18,29 @@ RSpec.describe Banzai::ReferenceParser::BaseParser do
parser_class.new(context)
end
+ describe '.reference_class' do
+ context 'when the method is not defined' do
+ it 'build the reference class' do
+ dummy = Class.new(described_class)
+ dummy.reference_type = :issue
+
+ expect(dummy.reference_class).to eq(Issue)
+ end
+ end
+
+ context 'when the method is redefined' do
+ it 'uses specified reference class' do
+ dummy = Class.new(described_class) do
+ def self.reference_class
+ AlertManagement::Alert
+ end
+ end
+
+ expect(dummy.reference_class).to eq(AlertManagement::Alert)
+ end
+ end
+ end
+
describe '.reference_type=' do
it 'sets the reference type' do
dummy = Class.new(described_class)
diff --git a/spec/lib/bitbucket_server/connection_spec.rb b/spec/lib/bitbucket_server/connection_spec.rb
index ae73955e1d1..8341ca10f43 100644
--- a/spec/lib/bitbucket_server/connection_spec.rb
+++ b/spec/lib/bitbucket_server/connection_spec.rb
@@ -56,6 +56,12 @@ RSpec.describe BitbucketServer::Connection do
expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError)
end
+
+ it 'throws an exception if the URI is invalid' do
+ stub_request(:post, url).with(headers: { 'Accept' => 'application/json' }).to_raise(URI::InvalidURIError)
+
+ expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError)
+ end
end
describe '#delete' do
diff --git a/spec/lib/bulk_imports/clients/http_spec.rb b/spec/lib/bulk_imports/clients/http_spec.rb
index 6962a943755..4fb08fc0478 100644
--- a/spec/lib/bulk_imports/clients/http_spec.rb
+++ b/spec/lib/bulk_imports/clients/http_spec.rb
@@ -9,13 +9,23 @@ RSpec.describe BulkImports::Clients::HTTP do
let(:token) { 'token' }
let(:resource) { 'resource' }
let(:version) { "#{BulkImport::MIN_MAJOR_VERSION}.0.0" }
+ let(:enterprise) { false }
let(:response_double) { double(code: 200, success?: true, parsed_response: {}) }
- let(:version_response) { double(code: 200, success?: true, parsed_response: { 'version' => version }) }
+ let(:metadata_response) do
+ double(
+ code: 200,
+ success?: true,
+ parsed_response: {
+ 'version' => version,
+ 'enterprise' => enterprise
+ }
+ )
+ end
before do
allow(Gitlab::HTTP).to receive(:get)
.with('http://gitlab.example/api/v4/version', anything)
- .and_return(version_response)
+ .and_return(metadata_response)
end
subject { described_class.new(url: url, token: token) }
@@ -213,13 +223,27 @@ RSpec.describe BulkImports::Clients::HTTP do
expect(Gitlab::HTTP).to receive(:get)
.with('http://gitlab.example/api/v4/metadata', anything)
- .and_return(version_response)
+ .and_return(metadata_response)
expect(subject.instance_version).to eq(Gitlab::VersionInfo.parse(version))
end
end
end
+ describe '#instance_enterprise' do
+ it 'returns source instance enterprise information' do
+ expect(subject.instance_enterprise).to eq(false)
+ end
+
+ context 'when enterprise information is missing' do
+ let(:enterprise) { nil }
+
+ it 'defaults to true' do
+ expect(subject.instance_enterprise).to eq(true)
+ end
+ end
+ end
+
describe '#compatible_for_project_migration?' do
context 'when instance version is lower the the expected minimum' do
it 'returns false' do
@@ -254,7 +278,7 @@ RSpec.describe BulkImports::Clients::HTTP do
before do
allow(Gitlab::HTTP).to receive(:get)
.with('http://website.example/gitlab/api/v4/version', anything)
- .and_return(version_response)
+ .and_return(metadata_response)
end
it 'performs network request to a relative gitlab url' do
diff --git a/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb
index 7a93365d098..35ca67c8a4c 100644
--- a/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
+RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline, feature_category: :import do
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
@@ -103,7 +103,9 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
context 'when dynamic path is nil' do
it 'returns' do
- expect { pipeline.load(context, File.join(tmpdir, 'test')) }.not_to change { portable.uploads.count }
+ path = File.join(tmpdir, 'test')
+ FileUtils.touch(path)
+ expect { pipeline.load(context, path) }.not_to change { portable.uploads.count }
end
end
@@ -122,6 +124,20 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
expect { pipeline.load(context, symlink) }.not_to change { portable.uploads.count }
end
end
+
+ context 'when path traverses' do
+ it 'does not upload the file' do
+ path_traversal = "#{uploads_dir_path}/avatar/../../../../etc/passwd"
+ expect { pipeline.load(context, path_traversal) }.to not_change { portable.uploads.count }.and raise_error(Gitlab::Utils::PathTraversalAttackError)
+ end
+ end
+
+ context 'when path is outside the tmpdir' do
+ it 'does not upload the file' do
+ path = "/etc/passwd"
+ expect { pipeline.load(context, path) }.to not_change { portable.uploads.count }.and raise_error(StandardError, /not allowed/)
+ end
+ end
end
describe '#after_run' do
diff --git a/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb
index 97fcddefd42..19e3a1fecc3 100644
--- a/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb
@@ -89,6 +89,7 @@ RSpec.describe BulkImports::Projects::Pipelines::IssuesPipeline do
expect(award_emoji.user).to eq(user)
end
end
+
context 'issue state' do
let(:issue_attributes) { { 'state' => 'closed' } }
diff --git a/spec/lib/extracts_ref_spec.rb b/spec/lib/extracts_ref_spec.rb
index 3e9a7499fdd..ca8af9413f3 100644
--- a/spec/lib/extracts_ref_spec.rb
+++ b/spec/lib/extracts_ref_spec.rb
@@ -44,6 +44,19 @@ RSpec.describe ExtractsRef do
expect { assign_ref_vars }.not_to raise_error
end
end
+
+ context 'when a ref_type parameter is provided' do
+ let(:params) { ActionController::Parameters.new(path: path, ref: ref, ref_type: 'tags') }
+
+ context 'and the use_ref_type_parameter feature flag is enabled' do
+ it 'sets a fully_qualified_ref variable' do
+ fully_qualified_ref = "refs/tags/#{ref}"
+ expect(container.repository).to receive(:commit).with(fully_qualified_ref)
+ assign_ref_vars
+ expect(@fully_qualified_ref).to eq(fully_qualified_ref)
+ end
+ end
+ end
end
it_behaves_like 'extracts refs'
diff --git a/spec/lib/feature/definition_spec.rb b/spec/lib/feature/definition_spec.rb
index 3d11ad4c0d8..595725d357c 100644
--- a/spec/lib/feature/definition_spec.rb
+++ b/spec/lib/feature/definition_spec.rb
@@ -25,6 +25,9 @@ RSpec.describe Feature::Definition do
using RSpec::Parameterized::TableSyntax
where(:param, :value, :result) do
+ :name | 'colon:separated' | /Feature flag 'colon:separated' is invalid/
+ :name | 'space separated' | /Feature flag 'space separated' is invalid/
+ :name | 'ALL_CAPS' | /Feature flag 'ALL_CAPS' is invalid/
:name | nil | /Feature flag is missing name/
:path | nil | /Feature flag 'feature_flag' is missing path/
:type | nil | /Feature flag 'feature_flag' is missing type/
@@ -111,6 +114,11 @@ RSpec.describe Feature::Definition do
subject { described_class.send(:load_all!) }
+ after do
+ FileUtils.rm_rf(store1)
+ FileUtils.rm_rf(store2)
+ end
+
it "when there's no feature flags a list of definitions is empty" do
is_expected.to be_empty
end
@@ -136,11 +144,6 @@ RSpec.describe Feature::Definition do
.to raise_error(/Feature flag is missing name/)
end
- after do
- FileUtils.rm_rf(store1)
- FileUtils.rm_rf(store2)
- end
-
def write_feature_flag(store, path, content)
path = File.join(store, path)
dir = File.dirname(path)
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index ad324406450..c087931d36a 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -162,6 +162,13 @@ RSpec.describe Feature, stub_feature_flags: false do
stub_feature_flag_definition(:enabled_feature_flag, default_enabled: true)
end
+ context 'when using redis cache', :use_clean_rails_redis_caching do
+ it 'does not make recursive feature-flag calls' do
+ expect(described_class).to receive(:enabled?).once.and_call_original
+ described_class.enabled?(:disabled_feature_flag)
+ end
+ end
+
context 'when self-recursive' do
before do
allow(Feature).to receive(:with_feature).and_wrap_original do |original, name, &block|
@@ -318,6 +325,31 @@ RSpec.describe Feature, stub_feature_flags: false do
end
end
+ context 'with a group member' do
+ let(:key) { :awesome_feature }
+ let(:guinea_pigs) { create_list(:user, 3) }
+
+ before do
+ described_class.reset
+ stub_feature_flag_definition(key)
+ Flipper.unregister_groups
+ Flipper.register(:guinea_pigs) do |actor|
+ guinea_pigs.include?(actor.thing)
+ end
+ described_class.enable(key, described_class.group(:guinea_pigs))
+ end
+
+ it 'is true for all group members' do
+ expect(described_class.enabled?(key, guinea_pigs[0])).to be_truthy
+ expect(described_class.enabled?(key, guinea_pigs[1])).to be_truthy
+ expect(described_class.enabled?(key, guinea_pigs[2])).to be_truthy
+ end
+
+ it 'is false for any other actor' do
+ expect(described_class.enabled?(key, create(:user))).to be_falsey
+ end
+ end
+
context 'with an individual actor' do
let(:actor) { stub_feature_flag_gate('CustomActor:5') }
let(:another_actor) { stub_feature_flag_gate('CustomActor:10') }
@@ -495,7 +527,7 @@ RSpec.describe Feature, stub_feature_flags: false do
let(:expected_extra) {}
it 'logs the event' do
- expect(Feature.logger).to receive(:info).with(key: key, action: expected_action, **expected_extra)
+ expect(Feature.logger).to receive(:info).at_least(:once).with(key: key, action: expected_action, **expected_extra)
subject
end
@@ -518,10 +550,10 @@ RSpec.describe Feature, stub_feature_flags: false do
end
context 'when thing is an actor' do
- let(:thing) { create(:project) }
+ let(:thing) { create(:user) }
it_behaves_like 'logging' do
- let(:expected_action) { :enable }
+ let(:expected_action) { eq(:enable) | eq(:remove_opt_out) }
let(:expected_extra) { { "extra.thing" => thing.flipper_id.to_s } }
end
end
@@ -544,12 +576,160 @@ RSpec.describe Feature, stub_feature_flags: false do
end
context 'when thing is an actor' do
- let(:thing) { create(:project) }
+ let(:thing) { create(:user) }
+ let(:flag_opts) { {} }
it_behaves_like 'logging' do
let(:expected_action) { :disable }
let(:expected_extra) { { "extra.thing" => thing.flipper_id.to_s } }
end
+
+ before do
+ stub_feature_flag_definition(key, flag_opts)
+ end
+
+ context 'when the feature flag was enabled for this actor' do
+ before do
+ described_class.enable(key, thing)
+ end
+
+ it 'marks this thing as disabled' do
+ expect { subject }.to change { thing_enabled? }.from(true).to(false)
+ end
+
+ it 'does not change the global value' do
+ expect { subject }.not_to change { described_class.enabled?(key) }.from(false)
+ end
+
+ it 'is possible to re-enable the feature' do
+ subject
+
+ expect { described_class.enable(key, thing) }
+ .to change { thing_enabled? }.from(false).to(true)
+ end
+ end
+
+ context 'when the feature flag is enabled globally' do
+ before do
+ described_class.enable(key)
+ end
+
+ it 'does not mark this thing as disabled' do
+ expect { subject }.not_to change { thing_enabled? }.from(true)
+ end
+
+ it 'does not change the global value' do
+ expect { subject }.not_to change { described_class.enabled?(key) }.from(true)
+ end
+ end
+ end
+ end
+
+ describe 'opt_out' do
+ subject { described_class.opt_out(key, thing) }
+
+ let(:key) { :awesome_feature }
+
+ before do
+ stub_feature_flag_definition(key)
+ described_class.enable(key)
+ end
+
+ context 'when thing is an actor' do
+ let_it_be(:thing) { create(:project) }
+
+ it 'marks this thing as disabled' do
+ expect { subject }.to change { thing_enabled? }.from(true).to(false)
+ end
+
+ it 'does not change the global value' do
+ expect { subject }.not_to change { described_class.enabled?(key) }.from(true)
+ end
+
+ it_behaves_like 'logging' do
+ let(:expected_action) { eq(:opt_out) }
+ let(:expected_extra) { { "extra.thing" => thing.flipper_id.to_s } }
+ end
+
+ it 'stores the opt-out information as a gate' do
+ subject
+
+ flag = described_class.get(key)
+
+ expect(flag.actors_value).to include(include(thing.flipper_id))
+ expect(flag.actors_value).not_to include(thing.flipper_id)
+ end
+ end
+
+ context 'when thing is a group' do
+ let(:thing) { Feature.group(:guinea_pigs) }
+ let(:guinea_pigs) { create_list(:user, 3) }
+
+ before do
+ Feature.reset
+ Flipper.unregister_groups
+ Flipper.register(:guinea_pigs) do |actor|
+ guinea_pigs.include?(actor.thing)
+ end
+ end
+
+ it 'has no effect' do
+ expect { subject }.not_to change { described_class.enabled?(key, guinea_pigs.first) }.from(true)
+ end
+ end
+ end
+
+ describe 'remove_opt_out' do
+ subject { described_class.remove_opt_out(key, thing) }
+
+ let(:key) { :awesome_feature }
+
+ before do
+ stub_feature_flag_definition(key)
+ described_class.enable(key)
+ described_class.opt_out(key, thing)
+ end
+
+ context 'when thing is an actor' do
+ let_it_be(:thing) { create(:project) }
+
+ it 're-enables this thing' do
+ expect { subject }.to change { thing_enabled? }.from(false).to(true)
+ end
+
+ it 'does not change the global value' do
+ expect { subject }.not_to change { described_class.enabled?(key) }.from(true)
+ end
+
+ it_behaves_like 'logging' do
+ let(:expected_action) { eq(:remove_opt_out) }
+ let(:expected_extra) { { "extra.thing" => thing.flipper_id.to_s } }
+ end
+
+ it 'removes the opt-out information' do
+ subject
+
+ flag = described_class.get(key)
+
+ expect(flag.actors_value).to be_empty
+ end
+ end
+
+ context 'when thing is a group' do
+ let(:thing) { Feature.group(:guinea_pigs) }
+ let(:guinea_pigs) { create_list(:user, 3) }
+
+ before do
+ Feature.reset
+ Flipper.unregister_groups
+ Flipper.register(:guinea_pigs) do |actor|
+ guinea_pigs.include?(actor.thing)
+ end
+ end
+
+ it 'has no effect' do
+ expect { subject }.not_to change { described_class.enabled?(key, guinea_pigs.first) }.from(true)
+ end
end
end
@@ -563,6 +743,16 @@ RSpec.describe Feature, stub_feature_flags: false do
let(:expected_action) { :enable_percentage_of_time }
let(:expected_extra) { { "extra.percentage" => percentage.to_s } }
end
+
+ context 'when the flag is on' do
+ before do
+ described_class.enable(key)
+ end
+
+ it 'fails with InvalidOperation' do
+ expect { subject }.to raise_error(described_class::InvalidOperation)
+ end
+ end
end
describe '.disable_percentage_of_time' do
@@ -586,6 +776,16 @@ RSpec.describe Feature, stub_feature_flags: false do
let(:expected_action) { :enable_percentage_of_actors }
let(:expected_extra) { { "extra.percentage" => percentage.to_s } }
end
+
+ context 'when the flag is on' do
+ before do
+ described_class.enable(key)
+ end
+
+ it 'fails with InvalidOperation' do
+ expect { subject }.to raise_error(described_class::InvalidOperation)
+ end
+ end
end
describe '.disable_percentage_of_actors' do
@@ -603,6 +803,7 @@ RSpec.describe Feature, stub_feature_flags: false do
subject { described_class.remove(key) }
let(:key) { :awesome_feature }
+ let(:actor) { create(:user) }
before do
described_class.enable(key)
@@ -617,13 +818,10 @@ RSpec.describe Feature, stub_feature_flags: false do
it 'returns nil' do
expect(described_class.remove(:non_persisted_feature_flag)).to be_nil
end
- end
- context 'for a persisted feature' do
- it 'returns true' do
- described_class.enable(:persisted_feature_flag)
-
- expect(described_class.remove(:persisted_feature_flag)).to be_truthy
+ it 'returns true, and cleans up' do
+ expect(subject).to be_truthy
+ expect(described_class.persisted_names).not_to include(key)
end
end
end
@@ -712,6 +910,10 @@ RSpec.describe Feature, stub_feature_flags: false do
end
end
+ before do
+ stub_feature_flag_definition(:enabled_feature_flag)
+ end
+
it 'gives the correct value when enabling for an additional actor' do
described_class.enable(:enabled_feature_flag, actor)
initial_gate_values = active_record_adapter.get(described_class.get(:enabled_feature_flag))
@@ -834,4 +1036,8 @@ RSpec.describe Feature, stub_feature_flags: false do
end
end
end
+
+ def thing_enabled?
+ described_class.enabled?(key, thing)
+ end
end
diff --git a/spec/lib/generators/model/mocks/migration_file.txt b/spec/lib/generators/model/mocks/migration_file.txt
index e92c2d2b534..091e086ba65 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.0]
+class CreateModelGeneratorTestFoos < Gitlab::Database::Migration[2.1]
# 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.
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb
index 24f8fb40445..271022e7c55 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb
@@ -22,10 +22,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder do
project.add_maintainer(user)
mr1.metrics.update!(merged_at: 1.month.ago)
mr2.metrics.update!(merged_at: Time.now)
- end
-
- around do |example|
- Timecop.freeze { example.run }
+ freeze_time
end
describe 'date range parameters' do
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb
index 258f4a0d019..4db5d64164e 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb
@@ -18,10 +18,6 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Median do
subject { described_class.new(stage: stage, query: query).seconds }
- around do |example|
- Timecop.freeze { example.run }
- end
-
it 'retruns nil when no results' do
expect(subject).to eq(nil)
end
@@ -30,11 +26,11 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Median do
merge_request1 = create(:merge_request, source_branch: '1', target_project: project, source_project: project)
merge_request2 = create(:merge_request, source_branch: '2', target_project: project, source_project: project)
- travel_to(5.minutes.from_now) do
+ travel(5.minutes) do
merge_request1.metrics.update!(merged_at: Time.zone.now)
end
- travel_to(10.minutes.from_now) do
+ travel(10.minutes) do
merge_request2.metrics.update!(merged_at: Time.zone.now)
end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
index 34d5158a5ab..7f70a4cfc4e 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do
- around do |example|
- Timecop.freeze { example.run }
+ before_all do
+ freeze_time
end
let(:params) { { from: 1.year.ago, current_user: user } }
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events_spec.rb
new file mode 100644
index 00000000000..f5a8035b9b4
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents, feature_category: :value_stream_management do
+ describe '#selectable_events' do
+ subject(:selectable_events) { described_class.selectable_events }
+
+ it 'excludes internal events' do
+ expect(selectable_events).to include(Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated)
+ expect(selectable_events).to exclude(Gitlab::Analytics::CycleAnalytics::StageEvents::IssueStageEnd)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/api_authentication/sent_through_builder_spec.rb b/spec/lib/gitlab/api_authentication/sent_through_builder_spec.rb
index 845e317f3aa..aa088a5d6d5 100644
--- a/spec/lib/gitlab/api_authentication/sent_through_builder_spec.rb
+++ b/spec/lib/gitlab/api_authentication/sent_through_builder_spec.rb
@@ -8,10 +8,10 @@ RSpec.describe Gitlab::APIAuthentication::SentThroughBuilder do
let(:locators) { Array.new(3) { double } }
it 'adds a strategy for each of locators x resolvers' do
- strategies = locators.to_h { |l| [l, []] }
+ strategies = locators.index_with { [] }
described_class.new(strategies, resolvers).sent_through(*locators)
- expect(strategies).to eq(locators.to_h { |l| [l, resolvers] })
+ expect(strategies).to eq(locators.index_with { resolvers })
end
end
end
diff --git a/spec/lib/gitlab/application_context_spec.rb b/spec/lib/gitlab/application_context_spec.rb
index 58d462aa27f..20c1536b9e6 100644
--- a/spec/lib/gitlab/application_context_spec.rb
+++ b/spec/lib/gitlab/application_context_spec.rb
@@ -141,7 +141,8 @@ RSpec.describe Gitlab::ApplicationContext do
describe 'setting the client' do
let_it_be(:remote_ip) { '127.0.0.1' }
let_it_be(:runner) { create(:ci_runner) }
- let_it_be(:options) { { remote_ip: remote_ip, runner: runner, user: user } }
+ let_it_be(:job) { create(:ci_build, :pending, :queued, user: user, project: project) }
+ let_it_be(:options) { { remote_ip: remote_ip, runner: runner, user: user, job: job } }
using RSpec::Parameterized::TableSyntax
@@ -150,6 +151,7 @@ RSpec.describe Gitlab::ApplicationContext do
[:remote_ip, :runner] | :runner
[:remote_ip, :runner, :user] | :runner
[:remote_ip, :user] | :user
+ [:job] | :user
end
with_them do
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index d2eb9209f42..3d9076c6fa7 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -697,7 +697,7 @@ module Gitlab
end
end
- shared_examples :invalid_include do
+ shared_examples 'invalid include' do
let(:include_path) { 'dk.png' }
before do
@@ -716,7 +716,7 @@ module Gitlab
context 'with path to a binary file' do
let(:blob) { fake_blob(path: 'dk.png', binary: true) }
- include_examples :invalid_include
+ include_examples 'invalid include'
end
context 'with path to file in external storage' do
@@ -727,7 +727,7 @@ module Gitlab
project.update_attribute(:lfs_enabled, true)
end
- include_examples :invalid_include
+ include_examples 'invalid include'
end
context 'with path to a textual file' do
@@ -737,7 +737,7 @@ module Gitlab
create_file(file_path, "Content from #{include_path}")
end
- shared_examples :valid_include do
+ shared_examples 'valid include' do
[
['/doc/sample.adoc', 'doc/sample.adoc', 'absolute path'],
['sample.adoc', 'doc/api/sample.adoc', 'relative path'],
@@ -760,24 +760,24 @@ module Gitlab
context 'when requested path is a file in the repo' do
let(:requested_path) { 'doc/api/README.adoc' }
- include_examples :valid_include
+ include_examples 'valid include'
context 'without a commit (only ref)' do
let(:commit) { nil }
- include_examples :valid_include
+ include_examples 'valid include'
end
end
context 'when requested path is a directory in the repo' do
let(:requested_path) { 'doc/api/' }
- include_examples :valid_include
+ include_examples 'valid include'
context 'without a commit (only ref)' do
let(:commit) { nil }
- include_examples :valid_include
+ include_examples 'valid include'
end
end
end
diff --git a/spec/lib/gitlab/audit/auditor_spec.rb b/spec/lib/gitlab/audit/auditor_spec.rb
index f743515e616..4b16333d913 100644
--- a/spec/lib/gitlab/audit/auditor_spec.rb
+++ b/spec/lib/gitlab/audit/auditor_spec.rb
@@ -68,6 +68,7 @@ RSpec.describe Gitlab::Audit::Auditor do
expect(logger).to have_received(:info).with(
hash_including(
+ 'id' => AuditEvent.last.id,
'author_id' => author.id,
'author_name' => author.name,
'entity_id' => group.id,
@@ -112,6 +113,7 @@ RSpec.describe Gitlab::Audit::Auditor do
expect(logger).to have_received(:info).with(
hash_including(
+ 'id' => AuditEvent.last.id,
'author_id' => author.id,
'author_name' => author.name,
'entity_id' => group.id,
@@ -244,7 +246,9 @@ RSpec.describe Gitlab::Audit::Auditor do
let(:audit!) { auditor.audit(context) }
before do
- allow(AuditEvent).to receive(:bulk_insert!).and_raise(ActiveRecord::RecordInvalid)
+ expect_next_instance_of(AuditEvent) do |instance|
+ allow(instance).to receive(:save!).and_raise(ActiveRecord::RecordInvalid)
+ end
allow(Gitlab::ErrorTracking).to receive(:track_exception)
end
@@ -261,5 +265,27 @@ RSpec.describe Gitlab::Audit::Auditor do
expect { auditor.audit(context) }.not_to raise_exception
end
end
+
+ context 'when audit event is not saved in database due to some database infra issue' do
+ let(:audit!) { auditor.audit(context) }
+
+ before do
+ allow_any_instance_of(auditor) do |auditor_instance|
+ allow(auditor_instance).to receive(:log_to_database).and_return(nil)
+ end
+ end
+
+ it 'calls log_to_file_and_stream with in memory events' do
+ audit!
+
+ expect_any_instance_of(auditor) do |auditor_instance|
+ expect(auditor_instance).to receive(:log_to_file_and_stream).with(include(kind_of(AuditEvent)))
+ end
+ end
+
+ it 'does not throw exception' do
+ expect { auditor.audit(context) }.not_to raise_exception
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/audit/type/definition_spec.rb b/spec/lib/gitlab/audit/type/definition_spec.rb
index 9f4282a4ec0..d1d6b0d7a78 100644
--- a/spec/lib/gitlab/audit/type/definition_spec.rb
+++ b/spec/lib/gitlab/audit/type/definition_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Audit::Type::Definition do
description: 'Group deploy token is deleted',
introduced_by_issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/1',
introduced_by_mr: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1',
- group: 'govern::compliance',
+ feature_category: 'continuous_delivery',
milestone: '15.4',
saved_to_database: true,
streamed: true }
@@ -18,6 +18,12 @@ RSpec.describe Gitlab::Audit::Type::Definition do
let(:definition) { described_class.new(path, attributes) }
let(:yaml_content) { attributes.deep_stringify_keys.to_yaml }
+ around do |example|
+ described_class.clear_memoization(:definitions)
+ example.run
+ described_class.clear_memoization(:definitions)
+ end
+
describe '#key' do
subject { definition.key }
@@ -36,7 +42,7 @@ RSpec.describe Gitlab::Audit::Type::Definition do
:description | nil | %r{property '/description' is not of type: string}
:introduced_by_issue | nil | %r{property '/introduced_by_issue' is not of type: string}
:introduced_by_mr | nil | %r{property '/introduced_by_mr' is not of type: string}
- :group | nil | %r{property '/group' is not of type: string}
+ :feature_category | nil | %r{property '/feature_category' is not of type: string}
:milestone | nil | %r{property '/milestone' is not of type: string}
end
# rubocop:enable Layout/LineLength
@@ -103,7 +109,7 @@ RSpec.describe Gitlab::Audit::Type::Definition do
expect(audit_event_type_definition.name).to eq "group_deploy_token_destroyed"
expect(audit_event_type_definition.description).to eq "Group deploy token is deleted"
- expect(audit_event_type_definition.group).to eq "govern::compliance"
+ expect(audit_event_type_definition.feature_category).to eq "continuous_delivery"
expect(audit_event_type_definition.milestone).to eq "15.4"
expect(audit_event_type_definition.saved_to_database).to be true
expect(audit_event_type_definition.streamed).to be true
@@ -111,6 +117,65 @@ RSpec.describe Gitlab::Audit::Type::Definition do
end
end
+ describe '.event_names' do
+ before do
+ allow(described_class).to receive(:definitions) do
+ { definition.key => definition }
+ end
+ end
+
+ it 'returns names of event types as string array' do
+ expect(described_class.event_names).to match_array([definition.attributes[:name]])
+ end
+ end
+
+ describe '.defined?' do
+ before do
+ allow(described_class).to receive(:definitions) do
+ { definition.key => definition }
+ end
+ end
+
+ it 'returns true if definition for the event name exists' do
+ expect(described_class.defined?('group_deploy_token_destroyed')).to be_truthy
+ end
+
+ it 'returns false if definition for the event name exists' do
+ expect(described_class.defined?('random_event_name')).to be_falsey
+ end
+ end
+
+ describe '.stream_only?' do
+ let(:stream_only_event_attributes) do
+ { name: 'policy_project_updated',
+ description: 'This event is triggered whenever the security policy project is updated for a project',
+ introduced_by_issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/2',
+ introduced_by_mr: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2',
+ feature_category: 'security_policy_management',
+ milestone: '15.6',
+ saved_to_database: false,
+ streamed: true }
+ end
+
+ let(:stream_only_event_path) { File.join('types', 'policy_project_updated.yml') }
+ let(:stream_only_event_definition) { described_class.new(stream_only_event_path, stream_only_event_attributes) }
+
+ before do
+ allow(described_class).to receive(:definitions) do
+ { definition.key => definition,
+ stream_only_event_definition.key => stream_only_event_definition }
+ end
+ end
+
+ it 'returns true for a stream only event' do
+ expect(described_class.stream_only?('group_deploy_token_destroyed')).to be_falsey
+ end
+
+ it 'returns false for an event that is saved to database' do
+ expect(described_class.stream_only?('policy_project_updated')).to be_truthy
+ end
+ end
+
describe '.load_from_file' do
it 'properly loads a definition from file' do
expect_file_read(path, content: yaml_content)
@@ -186,6 +251,12 @@ RSpec.describe Gitlab::Audit::Type::Definition do
end
end
+ describe 'validate that all the YAML definitions matches the audit event type schema' do
+ it 'successfully loads all the YAML definitions' do
+ expect { described_class.definitions }.not_to raise_error
+ end
+ end
+
describe '.definitions' do
let(:store1) { Dir.mktmpdir('path1') }
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 9283c31a207..484b4702497 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect(subject).to eq(user)
expect(@current_authenticated_job).to eq job
expect(subject).to be_from_ci_job_token
- expect(subject.ci_job_token_scope.source_project).to eq(job.project)
+ expect(subject.ci_job_token_scope.current_project).to eq(job.project)
end
end
@@ -100,7 +100,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect(subject).to eq(user)
expect(@current_authenticated_job).to eq job
expect(subject).to be_from_ci_job_token
- expect(subject.ci_job_token_scope.source_project).to eq(job.project)
+ expect(subject.ci_job_token_scope.current_project).to eq(job.project)
end
else
it 'returns nil' do
diff --git a/spec/lib/gitlab/auth/current_user_mode_spec.rb b/spec/lib/gitlab/auth/current_user_mode_spec.rb
index a21f0931b78..0a68a4a0ae2 100644
--- a/spec/lib/gitlab/auth/current_user_mode_spec.rb
+++ b/spec/lib/gitlab/auth/current_user_mode_spec.rb
@@ -194,10 +194,41 @@ RSpec.describe Gitlab::Auth::CurrentUserMode, :request_store do
it 'creates a timestamp in the session' do
subject.request_admin_mode!
+
subject.enable_admin_mode!(password: user.password)
expect(session).to include(expected_session_entry(be_within(1.second).of(Time.now)))
end
+
+ it 'returns true after successful enable' do
+ subject.request_admin_mode!
+
+ expect(subject.enable_admin_mode!(password: user.password)).to eq(true)
+ end
+
+ it 'returns false after unsuccessful enable' do
+ subject.request_admin_mode!
+
+ expect(subject.enable_admin_mode!(password: 'wrong password')).to eq(false)
+ end
+
+ context 'when user is not an admin' do
+ let(:user) { build_stubbed(:user) }
+
+ it 'returns false' do
+ subject.request_admin_mode!
+
+ expect(subject.enable_admin_mode!(password: user.password)).to eq(false)
+ end
+ end
+
+ context 'when admin mode is not requested' do
+ it 'raises error' do
+ expect do
+ subject.enable_admin_mode!(password: user.password)
+ end.to raise_error(Gitlab::Auth::CurrentUserMode::NotRequestedError)
+ end
+ end
end
describe '#disable_admin_mode!' do
diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb
index 3be983857bc..160fd78b2b9 100644
--- a/spec/lib/gitlab/auth/ldap/config_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/config_spec.rb
@@ -99,7 +99,7 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
expect { described_class.new }.to raise_error ArgumentError
end
- it 'works' do
+ it 'returns an instance of Gitlab::Auth::Ldap::Config' do
expect(config).to be_a described_class
end
@@ -122,7 +122,8 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
host: 'ldap.example.com',
port: 386,
hosts: nil,
- encryption: nil
+ encryption: nil,
+ instrumentation_service: ActiveSupport::Notifications
)
end
diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb
index b471a89b491..5771b1cd609 100644
--- a/spec/lib/gitlab/auth/ldap/user_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/user_spec.rb
@@ -133,7 +133,7 @@ RSpec.describe Gitlab::Auth::Ldap::User do
context 'when user confirmation email is enabled' do
before do
- stub_application_setting send_user_confirmation_email: true
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
end
it 'creates and confirms the user anyway' do
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index 95a518afcf1..bb81621ec92 100644
--- a/spec/lib/gitlab/auth/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do
context 'when user confirmation email is enabled' do
before do
- stub_application_setting send_user_confirmation_email: true
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
end
it 'creates and confirms the user anyway' do
diff --git a/spec/lib/gitlab/auth/saml/user_spec.rb b/spec/lib/gitlab/auth/saml/user_spec.rb
index 796512bc52b..a8a5d8ae5df 100644
--- a/spec/lib/gitlab/auth/saml/user_spec.rb
+++ b/spec/lib/gitlab/auth/saml/user_spec.rb
@@ -46,6 +46,10 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
context 'external groups' do
+ before do
+ stub_saml_group_config(%w(Interns))
+ end
+
context 'are defined' do
it 'marks the user as external' do
stub_saml_group_config(%w(Freelancers))
@@ -55,10 +59,6 @@ RSpec.describe Gitlab::Auth::Saml::User do
end
end
- before do
- stub_saml_group_config(%w(Interns))
- end
-
context 'are defined but the user does not belong there' do
it 'does not mark the user as external' do
saml_user.save # rubocop:disable Rails/SaveBang
@@ -317,7 +317,7 @@ RSpec.describe Gitlab::Auth::Saml::User do
context 'when user confirmation email is enabled' do
before do
- stub_application_setting send_user_confirmation_email: true
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
end
it 'creates and confirms the user anyway' do
diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
index b239de841b6..84f6411eae6 100644
--- a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
+++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
@@ -22,14 +22,14 @@ RSpec.describe Gitlab::Auth::UniqueIpsLimiter, :clean_gitlab_redis_shared_state
end
it 'resets count after specified time window' do
- Timecop.freeze do
+ freeze_time do
expect(described_class.update_and_return_ips_count(user.id, 'ip2')).to eq(1)
expect(described_class.update_and_return_ips_count(user.id, 'ip3')).to eq(2)
+ end
- travel_to(Time.now.utc + described_class.config.unique_ips_limit_time_window) do
- expect(described_class.update_and_return_ips_count(user.id, 'ip4')).to eq(1)
- expect(described_class.update_and_return_ips_count(user.id, 'ip5')).to eq(2)
- end
+ travel_to(Time.now.utc + described_class.config.unique_ips_limit_time_window) do
+ expect(described_class.update_and_return_ips_count(user.id, 'ip4')).to eq(1)
+ expect(described_class.update_and_return_ips_count(user.id, 'ip5')).to eq(2)
end
end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_environment_tiers_spec.rb b/spec/lib/gitlab/background_migration/backfill_environment_tiers_spec.rb
new file mode 100644
index 00000000000..788ed40b61e
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_environment_tiers_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillEnvironmentTiers,
+ :migration, schema: 20221205151917, feature_category: :continuous_delivery do
+ let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let!(:project) { table(:projects).create!(namespace_id: namespace.id, project_namespace_id: namespace.id) }
+
+ let(:migration) do
+ described_class.new(start_id: 1, end_id: 1000,
+ batch_table: :environments, batch_column: :id,
+ sub_batch_size: 10, pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ end
+
+ describe '#perform' do
+ let!(:production) { table(:environments).create!(name: 'production', slug: 'production', project_id: project.id) }
+ let!(:staging) { table(:environments).create!(name: 'staging', slug: 'staging', project_id: project.id) }
+ let!(:testing) { table(:environments).create!(name: 'testing', slug: 'testing', project_id: project.id) }
+
+ let!(:development) do
+ table(:environments).create!(name: 'development', slug: 'development', project_id: project.id)
+ end
+
+ let!(:other) { table(:environments).create!(name: 'other', slug: 'other', project_id: project.id) }
+
+ it 'backfill tiers for all environments in range' do
+ expect(production.tier).to be_nil
+ expect(staging.tier).to be_nil
+ expect(testing.tier).to be_nil
+ expect(development.tier).to be_nil
+ expect(other.tier).to be_nil
+
+ migration.perform
+
+ expect(production.reload.tier).to eq(described_class::PRODUCTION_TIER)
+ expect(staging.reload.tier).to eq(described_class::STAGING_TIER)
+ expect(testing.reload.tier).to eq(described_class::TESTING_TIER)
+ expect(development.reload.tier).to eq(described_class::DEVELOPMENT_TIER)
+ expect(other.reload.tier).to eq(described_class::OTHER_TIER)
+ end
+ end
+
+ # Equivalent to spec/models/environment_spec.rb#guess_tier
+ describe 'same behavior with guess tier' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:environment) { table(:environments).create!(name: name, slug: name, project_id: project.id) }
+
+ where(:name, :tier) do
+ 'review/feature' | described_class::DEVELOPMENT_TIER
+ 'review/product' | described_class::DEVELOPMENT_TIER
+ 'DEV' | described_class::DEVELOPMENT_TIER
+ 'development' | described_class::DEVELOPMENT_TIER
+ 'trunk' | described_class::DEVELOPMENT_TIER
+ 'dev' | described_class::DEVELOPMENT_TIER
+ 'review/app' | described_class::DEVELOPMENT_TIER
+ 'PRODUCTION' | described_class::PRODUCTION_TIER
+ 'prod' | described_class::PRODUCTION_TIER
+ 'prod-east-2' | described_class::PRODUCTION_TIER
+ 'us-prod-east' | described_class::PRODUCTION_TIER
+ 'fe-production' | described_class::PRODUCTION_TIER
+ 'test' | described_class::TESTING_TIER
+ 'TEST' | described_class::TESTING_TIER
+ 'testing' | described_class::TESTING_TIER
+ 'testing-prd' | described_class::TESTING_TIER
+ 'acceptance-testing' | described_class::TESTING_TIER
+ 'production-test' | described_class::TESTING_TIER
+ 'test-production' | described_class::TESTING_TIER
+ 'QC' | described_class::TESTING_TIER
+ 'qa-env-2' | described_class::TESTING_TIER
+ 'gstg' | described_class::STAGING_TIER
+ 'staging' | described_class::STAGING_TIER
+ 'stage' | described_class::STAGING_TIER
+ 'Model' | described_class::STAGING_TIER
+ 'MODL' | described_class::STAGING_TIER
+ 'Pre-production' | described_class::STAGING_TIER
+ 'pre' | described_class::STAGING_TIER
+ 'Demo' | described_class::STAGING_TIER
+ 'staging' | described_class::STAGING_TIER
+ 'pre-prod' | described_class::STAGING_TIER
+ 'blue-kit-stage' | described_class::STAGING_TIER
+ 'nonprod' | described_class::STAGING_TIER
+ 'nonlive' | described_class::STAGING_TIER
+ 'non-prod' | described_class::STAGING_TIER
+ 'non-live' | described_class::STAGING_TIER
+ 'gprd' | described_class::PRODUCTION_TIER
+ 'gprd-cny' | described_class::PRODUCTION_TIER
+ 'production' | described_class::PRODUCTION_TIER
+ 'Production' | described_class::PRODUCTION_TIER
+ 'PRODUCTION' | described_class::PRODUCTION_TIER
+ 'Production/eu' | described_class::PRODUCTION_TIER
+ 'production/eu' | described_class::PRODUCTION_TIER
+ 'PRODUCTION/EU' | described_class::PRODUCTION_TIER
+ 'productioneu' | described_class::PRODUCTION_TIER
+ 'store-produce' | described_class::PRODUCTION_TIER
+ 'unproductive' | described_class::PRODUCTION_TIER
+ 'production/www.gitlab.com' | described_class::PRODUCTION_TIER
+ 'prod' | described_class::PRODUCTION_TIER
+ 'PROD' | described_class::PRODUCTION_TIER
+ 'Live' | described_class::PRODUCTION_TIER
+ 'canary' | described_class::OTHER_TIER
+ 'other' | described_class::OTHER_TIER
+ 'EXP' | described_class::OTHER_TIER
+ 'something-else' | described_class::OTHER_TIER
+ end
+
+ with_them do
+ it 'backfill tiers for all environments in range' do
+ expect(environment.tier).to be_nil
+
+ migration.perform
+
+ expect(environment.reload.tier).to eq(tier)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb b/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb
index 8947262ae9e..479afb56210 100644
--- a/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb
@@ -23,6 +23,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillImportedIssueSearchData,
let!(:issue) do
table(:issues).create!(
project_id: project.id,
+ namespace_id: project.project_namespace_id,
title: 'Patterson',
description: FFaker::HipsterIpsum.paragraph
)
@@ -71,6 +72,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillImportedIssueSearchData,
let!(:issue2) do
table(:issues).create!(
project_id: project.id,
+ namespace_id: project.project_namespace_id,
title: 'Chatterton',
description: FFaker::HipsterIpsum.paragraph
)
diff --git a/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb b/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb
index 65f5f8368df..8db45ac0f57 100644
--- a/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillJiraTrackerDeploymentType2, :migration, schema: 20210301200959 do
- let_it_be(:jira_integration_temp) { described_class::JiraServiceTemp }
- let_it_be(:jira_tracker_data_temp) { described_class::JiraTrackerDataTemp }
- let_it_be(:atlassian_host) { 'https://api.atlassian.net' }
- let_it_be(:mixedcase_host) { 'https://api.AtlassiaN.nEt' }
- let_it_be(:server_host) { 'https://my.server.net' }
+ let!(:jira_integration_temp) { described_class::JiraServiceTemp }
+ let!(:jira_tracker_data_temp) { described_class::JiraTrackerDataTemp }
+ let!(:atlassian_host) { 'https://api.atlassian.net' }
+ let!(:mixedcase_host) { 'https://api.AtlassiaN.nEt' }
+ let!(:server_host) { 'https://my.server.net' }
let(:jira_integration) { jira_integration_temp.create!(type: 'JiraService', active: true, category: 'issue_tracker') }
diff --git a/spec/lib/gitlab/background_migration/backfill_project_namespace_details_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_namespace_details_spec.rb
index 77d6cc43114..01daf16d10c 100644
--- a/spec/lib/gitlab/background_migration/backfill_project_namespace_details_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_project_namespace_details_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillProjectNamespaceDetails, :migration do
- let_it_be(:namespace_details) { table(:namespace_details) }
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:projects) { table(:projects) }
+ let!(:namespace_details) { table(:namespace_details) }
+ let!(:namespaces) { table(:namespaces) }
+ let!(:projects) { table(:projects) }
subject(:perform_migration) do
described_class.new(start_id: projects.minimum(:id),
diff --git a/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb
index 3ca7d28f09d..5fa92759cf9 100644
--- a/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb
@@ -1,12 +1,14 @@
# frozen_string_literal: true
require 'spec_helper'
-# todo: this will need to specify schema version once we introduce the not null constraint on issues#namespace_id
-# https://gitlab.com/gitlab-org/gitlab/-/issues/367835
-RSpec.describe Gitlab::BackgroundMigration::BackfillProjectNamespaceOnIssues do
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillProjectNamespaceOnIssues,
+ :migration, schema: 20221118103352, feature_category: :team_planning do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:issues) { table(:issues) }
+ let(:issue_base_type_enum_value) { 0 }
+ let(:issue_type) { table(:work_item_types).find_by!(namespace_id: nil, base_type: issue_base_type_enum_value) }
let(:namespace1) { namespaces.create!(name: 'batchtest1', type: 'Group', path: 'space1') }
let(:namespace2) { namespaces.create!(name: 'batchtest2', type: 'Group', parent_id: namespace1.id, path: 'space2') }
@@ -18,12 +20,12 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillProjectNamespaceOnIssues do
let(:proj1) { projects.create!(name: 'proj1', path: 'proj1', namespace_id: namespace1.id, project_namespace_id: proj_namespace1.id) }
let(:proj2) { projects.create!(name: 'proj2', path: 'proj2', namespace_id: namespace2.id, project_namespace_id: proj_namespace2.id) }
- let!(:proj1_issue_with_namespace) { issues.create!(title: 'issue1', project_id: proj1.id, namespace_id: proj_namespace1.id) }
- let!(:proj1_issue_without_namespace1) { issues.create!(title: 'issue2', project_id: proj1.id) }
- let!(:proj1_issue_without_namespace2) { issues.create!(title: 'issue3', project_id: proj1.id) }
- let!(:proj2_issue_with_namespace) { issues.create!(title: 'issue4', project_id: proj2.id, namespace_id: proj_namespace2.id) }
- let!(:proj2_issue_without_namespace1) { issues.create!(title: 'issue5', project_id: proj2.id) }
- let!(:proj2_issue_without_namespace2) { issues.create!(title: 'issue6', project_id: proj2.id) }
+ let!(:proj1_issue_with_namespace) { issues.create!(title: 'issue1', project_id: proj1.id, namespace_id: proj_namespace1.id, work_item_type_id: issue_type.id) }
+ let!(:proj1_issue_without_namespace1) { issues.create!(title: 'issue2', project_id: proj1.id, work_item_type_id: issue_type.id) }
+ let!(:proj1_issue_without_namespace2) { issues.create!(title: 'issue3', project_id: proj1.id, work_item_type_id: issue_type.id) }
+ let!(:proj2_issue_with_namespace) { issues.create!(title: 'issue4', project_id: proj2.id, namespace_id: proj_namespace2.id, work_item_type_id: issue_type.id) }
+ let!(:proj2_issue_without_namespace1) { issues.create!(title: 'issue5', project_id: proj2.id, work_item_type_id: issue_type.id) }
+ let!(:proj2_issue_without_namespace2) { issues.create!(title: 'issue6', project_id: proj2.id, work_item_type_id: issue_type.id) }
# rubocop:enable Layout/LineLength
let(:migration) do
diff --git a/spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb b/spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb
index 6ef474ad7f9..5f93424faf6 100644
--- a/spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillWorkItemTypeIdForIssues, :mi
it 'sets work_item_type_id only for the given type' do
expect(all_issues).to all(have_attributes(work_item_type_id: nil))
- expect { migrate }.to make_queries_matching(/UPDATE \"issues\" SET "work_item_type_id"/, 2)
+ expect { migrate }.to make_queries_matching(/UPDATE "issues" SET "work_item_type_id"/, 2)
all_issues.each(&:reload)
expect([issue1, issue2, issue3]).to all(have_attributes(work_item_type_id: issue_type.id))
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 95be14cefb1..7280ca0b58e 100644
--- a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
+++ b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
@@ -158,6 +158,28 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
end
end
+ describe '.feature_category' do
+ context 'when jobs does not have feature_category attribute set' do
+ let(:job_class) { Class.new(described_class) }
+
+ it 'returns :database as default' do
+ expect(job_class.feature_category).to eq(:database)
+ end
+ end
+
+ context 'when jobs have feature_category attribute set' do
+ let(:job_class) do
+ Class.new(described_class) do
+ feature_category :delivery
+ end
+ end
+
+ it 'returns the provided value' do
+ expect(job_class.feature_category).to eq(:delivery)
+ end
+ end
+ end
+
describe 'descendants', :eager_load do
it 'have the same method signature for #perform' do
expected_arity = described_class.instance_method(:perform).arity
diff --git a/spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb
index 1a00fd7c8b3..72958700ca2 100644
--- a/spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb
+++ b/spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb
@@ -7,6 +7,8 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::LooseIndexScanBa
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:issues) { table(:issues) }
+ let(:issue_base_type_enum_value) { 0 }
+ let(:issue_type) { table(:work_item_types).find_by!(namespace_id: nil, base_type: issue_base_type_enum_value) }
let!(:namespace1) { namespaces.create!(name: 'ns1', path: 'ns1') }
let!(:namespace2) { namespaces.create!(name: 'ns2', path: 'ns2') }
@@ -19,13 +21,15 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::LooseIndexScanBa
let!(:project4) { projects.create!(name: 'p4', namespace_id: namespace4.id, project_namespace_id: namespace4.id) }
let!(:project5) { projects.create!(name: 'p5', namespace_id: namespace5.id, project_namespace_id: namespace5.id) }
- let!(:issue1) { issues.create!(title: 'title', description: 'description', project_id: project2.id) }
- let!(:issue2) { issues.create!(title: 'title', description: 'description', project_id: project1.id) }
- let!(:issue3) { issues.create!(title: 'title', description: 'description', project_id: project2.id) }
- let!(:issue4) { issues.create!(title: 'title', description: 'description', project_id: project3.id) }
- let!(:issue5) { issues.create!(title: 'title', description: 'description', project_id: project2.id) }
- let!(:issue6) { issues.create!(title: 'title', description: 'description', project_id: project4.id) }
- let!(:issue7) { issues.create!(title: 'title', description: 'description', project_id: project5.id) }
+ # rubocop:disable Layout/LineLength
+ let!(:issue1) { issues.create!(title: 'title', description: 'description', project_id: project2.id, namespace_id: project2.project_namespace_id, work_item_type_id: issue_type.id) }
+ let!(:issue2) { issues.create!(title: 'title', description: 'description', project_id: project1.id, namespace_id: project1.project_namespace_id, work_item_type_id: issue_type.id) }
+ let!(:issue3) { issues.create!(title: 'title', description: 'description', project_id: project2.id, namespace_id: project2.project_namespace_id, work_item_type_id: issue_type.id) }
+ let!(:issue4) { issues.create!(title: 'title', description: 'description', project_id: project3.id, namespace_id: project3.project_namespace_id, work_item_type_id: issue_type.id) }
+ let!(:issue5) { issues.create!(title: 'title', description: 'description', project_id: project2.id, namespace_id: project2.project_namespace_id, work_item_type_id: issue_type.id) }
+ let!(:issue6) { issues.create!(title: 'title', description: 'description', project_id: project4.id, namespace_id: project4.project_namespace_id, work_item_type_id: issue_type.id) }
+ let!(:issue7) { issues.create!(title: 'title', description: 'description', project_id: project5.id, namespace_id: project5.project_namespace_id, work_item_type_id: issue_type.id) }
+ # rubocop:enable Layout/LineLength
it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchingStrategies::BaseStrategy }
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 afa955a6056..c03962c8d21 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
@@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabilities, :migration do
include MigrationHelpers::VulnerabilitiesHelper
- let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
- let_it_be(:users) { table(:users) }
- let_it_be(:user) do
+ let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let!(:users) { table(:users) }
+ let!(:user) do
users.create!(
name: "Example User",
email: "user@example.com",
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabili
)
end
- let_it_be(:project) do
+ let!(:project) do
table(:projects).create!(
id: 123,
namespace_id: namespace.id,
@@ -25,9 +25,9 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabili
)
end
- let_it_be(:scanners) { table(:vulnerability_scanners) }
- let_it_be(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
- let_it_be(:different_scanner) do
+ let!(:scanners) { table(:vulnerability_scanners) }
+ let!(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let!(:different_scanner) do
scanners.create!(
project_id: project.id,
external_id: 'test 2',
@@ -35,22 +35,22 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabili
)
end
- let_it_be(:vulnerabilities) { table(:vulnerabilities) }
- let_it_be(:vulnerability_with_finding) do
+ let!(:vulnerabilities) { table(:vulnerabilities) }
+ let!(:vulnerability_with_finding) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
- let_it_be(:vulnerability_without_finding) do
+ let!(:vulnerability_without_finding) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
- let_it_be(:cis_vulnerability_without_finding) do
+ let!(:cis_vulnerability_without_finding) do
create_vulnerability!(
project_id: project.id,
author_id: user.id,
@@ -58,7 +58,7 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabili
)
end
- let_it_be(:custom_vulnerability_without_finding) do
+ let!(:custom_vulnerability_without_finding) do
create_vulnerability!(
project_id: project.id,
author_id: user.id,
@@ -66,8 +66,8 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabili
)
end
- let_it_be(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
- let_it_be(:primary_identifier) do
+ let!(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+ let!(:primary_identifier) do
vulnerability_identifiers.create!(
project_id: project.id,
external_type: 'uuid-v5',
@@ -76,8 +76,8 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabili
name: 'Identifier for UUIDv5')
end
- let_it_be(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
- let_it_be(:finding) do
+ let!(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let!(:finding) do
create_finding!(
vulnerability_id: vulnerability_with_finding.id,
project_id: project.id,
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
new file mode 100644
index 00000000000..c5b46d3f57c
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_orphans_approval_merge_request_rules_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteOrphansApprovalMergeRequestRules do
+ describe '#perform' do
+ let(:batch_table) { :approval_merge_request_rules }
+ let(:batch_column) { :id }
+ let(:sub_batch_size) { 1 }
+ let(:pause_ms) { 0 }
+ let(:connection) { ApplicationRecord.connection }
+
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:approval_merge_request_rules) { table(:approval_merge_request_rules) }
+ let(:security_orchestration_policy_configurations) { table(:security_orchestration_policy_configurations) }
+ let(:namespace) { namespaces.create!(name: 'name', path: 'path') }
+ let(:project) do
+ projects
+ .create!(name: "project", path: "project", namespace_id: namespace.id, project_namespace_id: namespace.id)
+ end
+
+ 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)
+ end
+
+ let!(:security_orchestration_policy_configuration) do
+ security_orchestration_policy_configurations
+ .create!(project_id: project.id, security_policy_management_project_id: security_project.id)
+ end
+
+ let(:merge_request) do
+ table(:merge_requests).create!(target_project_id: project.id, target_branch: 'main', source_branch: 'feature')
+ end
+
+ let!(:approval_rule) do
+ approval_merge_request_rules.create!(
+ name: 'rule',
+ merge_request_id: merge_request.id,
+ report_type: 4,
+ security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id)
+ end
+
+ let!(:approval_rule_other_report_type) do
+ approval_merge_request_rules.create!(
+ name: 'rule 2',
+ merge_request_id: merge_request.id,
+ report_type: 1,
+ security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id)
+ end
+
+ let!(:approval_rule_last) do
+ approval_merge_request_rules.create!(name: 'rule 3', merge_request_id: merge_request.id, report_type: 4)
+ end
+
+ subject do
+ described_class.new(
+ start_id: approval_rule.id,
+ end_id: approval_rule_last.id,
+ batch_table: batch_table,
+ batch_column: batch_column,
+ sub_batch_size: sub_batch_size,
+ pause_ms: pause_ms,
+ connection: connection
+ ).perform
+ end
+
+ it 'delete only approval rules without association with the security project and report_type equals to 4' do
+ expect { subject }.to change { approval_merge_request_rules.count }.from(3).to(2)
+ end
+ end
+end
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
new file mode 100644
index 00000000000..16253255764
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_orphans_approval_project_rules_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteOrphansApprovalProjectRules do
+ describe '#perform' do
+ let(:batch_table) { :approval_project_rules }
+ let(:batch_column) { :id }
+ let(:sub_batch_size) { 1 }
+ let(:pause_ms) { 0 }
+ let(:connection) { ApplicationRecord.connection }
+
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:approval_project_rules) { table(:approval_project_rules) }
+ let(:security_orchestration_policy_configurations) { table(:security_orchestration_policy_configurations) }
+ let(:namespace) { namespaces.create!(name: 'name', path: 'path') }
+ let(:project) do
+ projects
+ .create!(name: "project", path: "project", namespace_id: namespace.id, project_namespace_id: namespace.id)
+ end
+
+ 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)
+ end
+
+ let!(:security_orchestration_policy_configuration) do
+ security_orchestration_policy_configurations
+ .create!(project_id: project.id, security_policy_management_project_id: security_project.id)
+ end
+
+ let!(:project_rule) do
+ approval_project_rules.create!(
+ name: 'rule',
+ project_id: project.id,
+ report_type: 4,
+ security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id)
+ end
+
+ let!(:project_rule_other_report_type) do
+ approval_project_rules.create!(
+ name: 'rule 2',
+ project_id: project.id,
+ report_type: 1,
+ security_orchestration_policy_configuration_id: security_orchestration_policy_configuration.id)
+ end
+
+ let!(:project_rule_last) do
+ approval_project_rules.create!(name: 'rule 3', project_id: project.id, report_type: 4)
+ end
+
+ subject do
+ described_class.new(
+ start_id: project_rule.id,
+ end_id: project_rule_last.id,
+ batch_table: batch_table,
+ batch_column: batch_column,
+ sub_batch_size: sub_batch_size,
+ pause_ms: pause_ms,
+ connection: connection
+ ).perform
+ end
+
+ it 'delete only approval rules without association with the security project and report_type equals to 4' do
+ expect { subject }.to change { approval_project_rules.count }.from(3).to(2)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images_spec.rb b/spec/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images_spec.rb
index 8a63673bf38..e7b0471810d 100644
--- a/spec/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images_spec.rb
+++ b/spec/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DisableExpirationPoliciesLinkedToNoContainerImages, :migration, schema: 20220326161803 do # rubocop:disable Layout/LineLength
- let_it_be(:projects) { table(:projects) }
- let_it_be(:container_expiration_policies) { table(:container_expiration_policies) }
- let_it_be(:container_repositories) { table(:container_repositories) }
- let_it_be(:namespaces) { table(:namespaces) }
+ let!(:projects) { table(:projects) }
+ let!(:container_expiration_policies) { table(:container_expiration_policies) }
+ let!(:container_repositories) { table(:container_repositories) }
+ let!(:namespaces) { table(:namespaces) }
let!(:namespace) { namespaces.create!(name: 'test', path: 'test') }
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 d20eaef3650..d60874c3159 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
@@ -50,7 +50,7 @@ RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForNoI
)
project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: repo_size)
- issues_table.create!(project_id: project.id) if with_issue
+ issues_table.create!(project_id: project.id, namespace_id: project.project_namespace_id) if with_issue
project_settings_table.create!(project_id: project.id, legacy_open_source_license_available: true)
project
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
new file mode 100644
index 00000000000..b92f1a74551
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DisableLegacyOpenSourceLicenseForProjectsLessThanFiveMb,
+ :migration,
+ schema: 20221018095434,
+ feature_category: :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
+ end
+
+ it 'sets `legacy_open_source_license_available` to false only for projects less than 5 MB', :aggregate_failures do
+ project_setting_2_mb = create_legacy_license_project_setting(repo_size: 2)
+ project_setting_4_mb = create_legacy_license_project_setting(repo_size: 4)
+ project_setting_5_mb = create_legacy_license_project_setting(repo_size: 5)
+ project_setting_6_mb = create_legacy_license_project_setting(repo_size: 6)
+
+ record = ActiveRecord::QueryRecorder.new do
+ expect { perform_migration }
+ .to change { migrated_attribute(project_setting_2_mb) }.from(true).to(false)
+ .and change { migrated_attribute(project_setting_4_mb) }.from(true).to(false)
+ .and not_change { migrated_attribute(project_setting_5_mb) }.from(true)
+ .and not_change { migrated_attribute(project_setting_6_mb) }.from(true)
+ end
+
+ expect(record.count).to eq(15)
+ end
+
+ private
+
+ # @param repo_size: Repo size in MB
+ 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)
+
+ size_in_bytes = 1.megabyte * repo_size
+ project_statistics_table.create!(project_id: project.id, namespace_id: namespace.id, repository_size: size_in_bytes)
+ project_settings_table.create!(project_id: project.id, legacy_open_source_license_available: true)
+ end
+
+ def migrated_attribute(project_setting)
+ project_settings_table.find(project_setting.project_id).legacy_open_source_license_available
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb
index 5b6722a3384..ba04f2d20a7 100644
--- a/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb
+++ b/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb
@@ -3,33 +3,33 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DropInvalidVulnerabilities, schema: 20210301200959 do
- let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
- let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
- let_it_be(:users) { table(:users) }
- let_it_be(:user) { create_user! }
- let_it_be(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
-
- let_it_be(:scanners) { table(:vulnerability_scanners) }
- let_it_be(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
- let_it_be(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
-
- let_it_be(:vulnerabilities) { table(:vulnerabilities) }
- let_it_be(:vulnerability_with_finding) do
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
+ let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let!(:users) { table(:users) }
+ let!(:user) { create_user! }
+ let!(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+
+ let!(:scanners) { table(:vulnerability_scanners) }
+ let!(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let!(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
+
+ let!(:vulnerabilities) { table(:vulnerabilities) }
+ let!(:vulnerability_with_finding) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
- let_it_be(:vulnerability_without_finding) do
+ let!(:vulnerability_without_finding) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
- let_it_be(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
- let_it_be(:primary_identifier) do
+ let!(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+ let!(:primary_identifier) do
vulnerability_identifiers.create!(
project_id: project.id,
external_type: 'uuid-v5',
@@ -38,8 +38,8 @@ RSpec.describe Gitlab::BackgroundMigration::DropInvalidVulnerabilities, schema:
name: 'Identifier for UUIDv5')
end
- let_it_be(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
- let_it_be(:finding) do
+ let!(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let!(:finding) do
create_finding!(
vulnerability_id: vulnerability_with_finding.id,
project_id: project.id,
@@ -94,7 +94,7 @@ RSpec.describe Gitlab::BackgroundMigration::DropInvalidVulnerabilities, schema:
vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
- metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
vulnerabilities_findings.create!(
vulnerability_id: vulnerability_id,
project_id: project_id,
diff --git a/spec/lib/gitlab/background_migration/populate_container_repository_migration_plan_spec.rb b/spec/lib/gitlab/background_migration/populate_container_repository_migration_plan_spec.rb
index 0463f5a0c0d..477167c9074 100644
--- a/spec/lib/gitlab/background_migration/populate_container_repository_migration_plan_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_container_repository_migration_plan_spec.rb
@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::PopulateContainerRepositoryMigrationPlan, schema: 20220316202640 do
- let_it_be(:container_repositories) { table(:container_repositories) }
- let_it_be(:projects) { table(:projects) }
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:gitlab_subscriptions) { table(:gitlab_subscriptions) }
- let_it_be(:plans) { table(:plans) }
- let_it_be(:namespace_statistics) { table(:namespace_statistics) }
+ let!(:container_repositories) { table(:container_repositories) }
+ let!(:projects) { table(:projects) }
+ let!(:namespaces) { table(:namespaces) }
+ let!(:gitlab_subscriptions) { table(:gitlab_subscriptions) }
+ let!(:plans) { table(:plans) }
+ let!(:namespace_statistics) { table(:namespace_statistics) }
let!(:namepace1) { namespaces.create!(id: 1, type: 'Group', name: 'group1', path: 'group1', traversal_ids: [1]) }
let!(:namepace2) { namespaces.create!(id: 2, type: 'Group', name: 'group2', path: 'group2', traversal_ids: [2]) }
diff --git a/spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb b/spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb
index 98b2bc437f3..4a7d52ee784 100644
--- a/spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::PopulateNamespaceStatistics do
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:namespace_statistics) { table(:namespace_statistics) }
- let_it_be(:dependency_proxy_manifests) { table(:dependency_proxy_manifests) }
- let_it_be(:dependency_proxy_blobs) { table(:dependency_proxy_blobs) }
+ let!(:namespaces) { table(:namespaces) }
+ let!(:namespace_statistics) { table(:namespace_statistics) }
+ let!(:dependency_proxy_manifests) { table(:dependency_proxy_manifests) }
+ let!(:dependency_proxy_blobs) { table(:dependency_proxy_blobs) }
let!(:group1) { namespaces.create!(id: 10, type: 'Group', name: 'group1', path: 'group1') }
let!(:group2) { namespaces.create!(id: 20, type: 'Group', name: 'group2', path: 'group2') }
diff --git a/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb b/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb
index fc06012ed20..c0470f26d9e 100644
--- a/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb
@@ -68,7 +68,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateVulnerabilityReads, :migrati
# rubocop:disable Metrics/ParameterLists
def create_finding!(
- vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
diff --git a/spec/lib/gitlab/background_migration/prune_stale_project_export_jobs_spec.rb b/spec/lib/gitlab/background_migration/prune_stale_project_export_jobs_spec.rb
new file mode 100644
index 00000000000..5150d0ea4b0
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/prune_stale_project_export_jobs_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::PruneStaleProjectExportJobs, feature_category: :importers do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:project_export_jobs) { table(:project_export_jobs) }
+ let(:project_relation_exports) { table(:project_relation_exports) }
+ let(:uploads) { table(:project_relation_export_uploads) }
+
+ subject(:perform_migration) do
+ described_class.new(start_id: 1,
+ end_id: 300,
+ batch_table: :project_export_jobs,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection)
+ .perform
+ end
+
+ it 'removes export jobs and associated relations older than 7 days' do
+ namespaces.create!(id: 1000, name: "Sally", path: 'sally')
+ projects.create!(id: 1, namespace_id: 1000, project_namespace_id: 1000)
+
+ project = Project.find 1
+
+ project_export_jobs.create!(id: 10, project_id: project.id, jid: SecureRandom.hex(10), updated_at: 37.months.ago)
+ project_export_jobs.create!(id: 20, project_id: project.id, jid: SecureRandom.hex(10), updated_at: 12.months.ago)
+ project_export_jobs.create!(id: 30, project_id: project.id, jid: SecureRandom.hex(10), updated_at: 8.days.ago)
+ project_export_jobs.create!(id: 40, project_id: project.id, jid: SecureRandom.hex(10), updated_at: 1.day.ago)
+ project_export_jobs.create!(id: 50, project_id: project.id, jid: SecureRandom.hex(10), updated_at: 2.days.ago)
+ project_export_jobs.create!(id: 60, project_id: project.id, jid: SecureRandom.hex(10), updated_at: 6.days.ago)
+
+ project_relation_exports.create!(id: 100, project_export_job_id: 10, relation: 'Project')
+ project_relation_exports.create!(id: 200, project_export_job_id: 20, relation: 'Project')
+ project_relation_exports.create!(id: 300, project_export_job_id: 30, relation: 'Project')
+ project_relation_exports.create!(id: 400, project_export_job_id: 40, relation: 'Project')
+ project_relation_exports.create!(id: 500, project_export_job_id: 50, relation: 'Project')
+ project_relation_exports.create!(id: 600, project_export_job_id: 60, relation: 'Project')
+
+ uploads.create!(project_relation_export_id: 100, export_file: "#{SecureRandom.alphanumeric(5)}_export.tar.gz")
+ uploads.create!(project_relation_export_id: 200, export_file: "#{SecureRandom.alphanumeric(5)}_export.tar.gz")
+ uploads.create!(project_relation_export_id: 300, export_file: "#{SecureRandom.alphanumeric(5)}_export.tar.gz")
+ uploads.create!(project_relation_export_id: 400, export_file: "#{SecureRandom.alphanumeric(5)}_export.tar.gz")
+ uploads.create!(project_relation_export_id: 500, export_file: "#{SecureRandom.alphanumeric(5)}_export.tar.gz")
+ uploads.create!(project_relation_export_id: 600, export_file: "#{SecureRandom.alphanumeric(5)}_export.tar.gz")
+
+ expect(project_export_jobs.all.size).to eq(6)
+ expect(project_relation_exports.all.size).to eq(6)
+ expect(uploads.all.size).to eq(6)
+
+ expect { perform_migration }
+ .to change { project_export_jobs.count }.by(-3)
+ .and change { project_relation_exports.count }.by(-3)
+ .and change { uploads.count }.by(-3)
+
+ expect(project_export_jobs.all.size).to eq(3)
+ expect(project_relation_exports.all.size).to eq(3)
+ expect(uploads.all.size).to eq(3)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
index 29cc4f34f6d..2271bbfb2f3 100644
--- a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
+++ b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
@@ -488,11 +488,10 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
# rubocop:disable Metrics/ParameterLists
def create_finding!(
- id: nil,
- vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, id: nil,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
- metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
vulnerability_findings.create!({
id: id,
vulnerability_id: vulnerability_id,
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 10597e65910..5fede892463 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
@@ -20,8 +20,8 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveBackfilledJobArtifactsExpireAt
)
end
- let_it_be(:namespace) { table(:namespaces).create!(id: 1, name: 'user', path: 'user') }
- let_it_be(:project) do
+ let!(:namespace) { table(:namespaces).create!(id: 1, name: 'user', path: 'user') }
+ let!(:project) do
table(:projects).create!(
id: 1,
name: 'gitlab1',
diff --git a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
index 8003159f59e..ed08ae22245 100644
--- a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
+++ b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
@@ -134,11 +134,10 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindin
# rubocop:disable Metrics/ParameterLists
def create_finding!(
- id: nil,
- vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, id: nil,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
- metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
params = {
vulnerability_id: vulnerability_id,
project_id: project_id,
diff --git a/spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb b/spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb
index 33ad74fbee8..1844347f4a9 100644
--- a/spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb
+++ b/spec/lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb
@@ -89,7 +89,6 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveOccurrencePipelinesAndDuplicat
let!(:unrelated_finding) do
create_finding!(
id: 9999999,
- uuid: "unreleated_finding",
vulnerability_id: nil,
report_type: 1,
location_fingerprint: 'random_location_fingerprint',
@@ -133,11 +132,10 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveOccurrencePipelinesAndDuplicat
# rubocop:disable Metrics/ParameterLists
def create_finding!(
- id: nil,
- vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, id: nil,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
- metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
params = {
vulnerability_id: vulnerability_id,
project_id: project_id,
diff --git a/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb b/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb
index ccf96e036ae..918df8f4442 100644
--- a/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb
+++ b/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveVulnerabilityFindingLinks, :mi
location_fingerprint: "location_fingerprint_#{id}",
metadata_version: 'metadata_version',
raw_metadata: 'raw_metadata',
- uuid: "uuid_#{id}"
+ uuid: SecureRandom.uuid
)
end
end
diff --git a/spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb b/spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb
index 45932defaf9..580465df4d9 100644
--- a/spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb
+++ b/spec/lib/gitlab/background_migration/rename_task_system_note_to_checklist_item_spec.rb
@@ -5,10 +5,22 @@ require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::RenameTaskSystemNoteToChecklistItem do
let(:notes) { table(:notes) }
let(:projects) { table(:projects) }
-
let(:namespace) { table(:namespaces).create!(name: 'batchtest1', type: 'Group', path: 'space1') }
- let(:project) { table(:projects).create!(name: 'proj1', path: 'proj1', namespace_id: namespace.id) }
- let(:issue) { table(:issues).create!(title: 'Test issue') }
+ let(:issue_base_type_enum_value) { 0 }
+ let(:issue_type) { table(:work_item_types).find_by!(namespace_id: nil, base_type: issue_base_type_enum_value) }
+
+ let(:project) do
+ table(:projects).create!(
+ name: 'proj1', path: 'proj1', namespace_id: namespace.id, project_namespace_id: namespace.id
+ )
+ end
+
+ let(:issue) do
+ table(:issues).create!(
+ title: 'Test issue', project_id: project.id,
+ namespace_id: project.project_namespace_id, work_item_type_id: issue_type.id
+ )
+ end
let!(:note1) do
notes.create!(
diff --git a/spec/lib/gitlab/background_migration/reset_status_on_container_repositories_spec.rb b/spec/lib/gitlab/background_migration/reset_status_on_container_repositories_spec.rb
new file mode 100644
index 00000000000..d50b04857d6
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/reset_status_on_container_repositories_spec.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::ResetStatusOnContainerRepositories, feature_category: :container_registry do
+ let(:projects_table) { table(:projects) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:container_repositories_table) { table(:container_repositories) }
+ let(:routes_table) { table(:routes) }
+
+ let!(:root_group) do
+ namespaces_table.create!(name: 'root_group', path: 'root_group', type: 'Group') do |new_group|
+ new_group.update!(traversal_ids: [new_group.id])
+ end
+ end
+
+ let!(:group1) do
+ namespaces_table.create!(name: 'group1', path: 'group1', parent_id: root_group.id, type: 'Group') do |new_group|
+ new_group.update!(traversal_ids: [root_group.id, new_group.id])
+ end
+ end
+
+ let!(:subgroup1) do
+ namespaces_table.create!(name: 'subgroup1', path: 'subgroup1', parent_id: group1.id, type: 'Group') do |new_group|
+ new_group.update!(traversal_ids: [root_group.id, group1.id, new_group.id])
+ end
+ end
+
+ let!(:group2) do
+ namespaces_table.create!(name: 'group2', path: 'group2', parent_id: root_group.id, type: 'Group') do |new_group|
+ new_group.update!(traversal_ids: [root_group.id, new_group.id])
+ end
+ end
+
+ let!(:group1_project_namespace) do
+ namespaces_table.create!(name: 'group1_project', path: 'group1_project', type: 'Project', parent_id: group1.id)
+ end
+
+ let!(:subgroup1_project_namespace) do
+ namespaces_table.create!(
+ name: 'subgroup1_project',
+ path: 'subgroup1_project',
+ type: 'Project',
+ parent_id: subgroup1.id
+ )
+ end
+
+ let!(:group2_project_namespace) do
+ namespaces_table.create!(
+ name: 'group2_project',
+ path: 'group2_project',
+ type: 'Project',
+ parent_id: group2.id
+ )
+ end
+
+ let!(:group1_project) do
+ projects_table.create!(
+ name: 'group1_project',
+ path: 'group1_project',
+ namespace_id: group1.id,
+ project_namespace_id: group1_project_namespace.id
+ )
+ end
+
+ let!(:subgroup1_project) do
+ projects_table.create!(
+ name: 'subgroup1_project',
+ path: 'subgroup1_project',
+ namespace_id: subgroup1.id,
+ project_namespace_id: subgroup1_project_namespace.id
+ )
+ end
+
+ let!(:group2_project) do
+ projects_table.create!(
+ name: 'group2_project',
+ path: 'group2_project',
+ namespace_id: group2.id,
+ project_namespace_id: group2_project_namespace.id
+ )
+ end
+
+ let!(:route2) do
+ routes_table.create!(
+ source_id: group2_project.id,
+ source_type: 'Project',
+ path: 'root_group/group2/group2_project',
+ namespace_id: group2_project_namespace.id
+ )
+ end
+
+ let!(:delete_scheduled_container_repository1) do
+ container_repositories_table.create!(project_id: group1_project.id, status: 0, name: 'container_repository1')
+ end
+
+ let!(:delete_scheduled_container_repository2) do
+ container_repositories_table.create!(project_id: subgroup1_project.id, status: 0, name: 'container_repository2')
+ end
+
+ let!(:delete_scheduled_container_repository3) do
+ container_repositories_table.create!(project_id: group2_project.id, status: 0, name: 'container_repository3')
+ end
+
+ let!(:delete_ongoing_container_repository4) do
+ container_repositories_table.create!(project_id: group2_project.id, status: 2, name: 'container_repository4')
+ end
+
+ let(:migration) do
+ described_class.new(
+ start_id: container_repositories_table.minimum(:id),
+ end_id: container_repositories_table.maximum(:id),
+ batch_table: :container_repositories,
+ batch_column: :id,
+ sub_batch_size: 50,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection
+ )
+ end
+
+ describe '#filter_batch' do
+ it 'scopes the relation to delete scheduled container repositories' do
+ expected = container_repositories_table.where(status: 0).pluck(:id)
+ actual = migration.filter_batch(container_repositories_table).pluck(:id)
+
+ expect(actual).to match_array(expected)
+ end
+ end
+
+ describe '#perform' do
+ let(:registry_api_url) { 'http://example.com' }
+
+ subject(:perform) { migration.perform }
+
+ before do
+ stub_container_registry_config(
+ enabled: true,
+ api_url: registry_api_url,
+ key: 'spec/fixtures/x509_certificate_pk.key'
+ )
+ stub_tags_list(path: 'root_group/group1/group1_project/container_repository1')
+ stub_tags_list(path: 'root_group/group1/subgroup1/subgroup1_project/container_repository2', tags: [])
+ stub_tags_list(path: 'root_group/group2/group2_project/container_repository3')
+ end
+
+ shared_examples 'resetting status of all container repositories scheduled for deletion' do
+ it 'resets all statuses' do
+ expect_logging_on(
+ path: 'root_group/group1/group1_project/container_repository1',
+ id: delete_scheduled_container_repository1.id,
+ has_tags: true
+ )
+ expect_logging_on(
+ path: 'root_group/group1/subgroup1/subgroup1_project/container_repository2',
+ id: delete_scheduled_container_repository2.id,
+ has_tags: true
+ )
+ expect_logging_on(
+ path: 'root_group/group2/group2_project/container_repository3',
+ id: delete_scheduled_container_repository3.id,
+ has_tags: true
+ )
+
+ expect { perform }
+ .to change { delete_scheduled_container_repository1.reload.status }.from(0).to(nil)
+ .and change { delete_scheduled_container_repository3.reload.status }.from(0).to(nil)
+ .and change { delete_scheduled_container_repository2.reload.status }.from(0).to(nil)
+ end
+ end
+
+ it 'resets status of container repositories with tags' do
+ expect_pull_access_token_on(path: 'root_group/group1/group1_project/container_repository1')
+ expect_pull_access_token_on(path: 'root_group/group1/subgroup1/subgroup1_project/container_repository2')
+ expect_pull_access_token_on(path: 'root_group/group2/group2_project/container_repository3')
+
+ expect_logging_on(
+ path: 'root_group/group1/group1_project/container_repository1',
+ id: delete_scheduled_container_repository1.id,
+ has_tags: true
+ )
+ expect_logging_on(
+ path: 'root_group/group1/subgroup1/subgroup1_project/container_repository2',
+ id: delete_scheduled_container_repository2.id,
+ has_tags: false
+ )
+ expect_logging_on(
+ path: 'root_group/group2/group2_project/container_repository3',
+ id: delete_scheduled_container_repository3.id,
+ has_tags: true
+ )
+
+ expect { perform }
+ .to change { delete_scheduled_container_repository1.reload.status }.from(0).to(nil)
+ .and change { delete_scheduled_container_repository3.reload.status }.from(0).to(nil)
+ .and not_change { delete_scheduled_container_repository2.reload.status }
+ end
+
+ context 'with the registry disabled' do
+ before do
+ allow(::Gitlab.config.registry).to receive(:enabled).and_return(false)
+ end
+
+ it_behaves_like 'resetting status of all container repositories scheduled for deletion'
+ end
+
+ context 'with the registry api url not defined' do
+ before do
+ allow(::Gitlab.config.registry).to receive(:api_url).and_return('')
+ end
+
+ it_behaves_like 'resetting status of all container repositories scheduled for deletion'
+ end
+
+ context 'with a faraday error' do
+ before do
+ client_double = instance_double('::ContainerRegistry::Client')
+ allow(::ContainerRegistry::Client).to receive(:new).and_return(client_double)
+ allow(client_double).to receive(:repository_tags).and_raise(Faraday::TimeoutError)
+
+ expect_pull_access_token_on(path: 'root_group/group1/group1_project/container_repository1')
+ expect_pull_access_token_on(path: 'root_group/group1/subgroup1/subgroup1_project/container_repository2')
+ expect_pull_access_token_on(path: 'root_group/group2/group2_project/container_repository3')
+ end
+
+ it_behaves_like 'resetting status of all container repositories scheduled for deletion'
+ end
+
+ def stub_tags_list(path:, tags: %w[tag1])
+ url = "#{registry_api_url}/v2/#{path}/tags/list?n=1"
+
+ stub_request(:get, url)
+ .with(
+ headers: {
+ 'Accept' => ContainerRegistry::Client::ACCEPTED_TYPES.join(', '),
+ 'Authorization' => /bearer .+/,
+ 'User-Agent' => "GitLab/#{Gitlab::VERSION}"
+ }
+ )
+ .to_return(
+ status: 200,
+ body: Gitlab::Json.dump(tags: tags),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ def expect_pull_access_token_on(path:)
+ expect(Auth::ContainerRegistryAuthenticationService)
+ .to receive(:pull_access_token).with(path).and_call_original
+ end
+
+ def expect_logging_on(path:, id:, has_tags:)
+ expect(::Gitlab::BackgroundMigration::Logger)
+ .to receive(:info).with(
+ migrator: described_class::MIGRATOR,
+ has_tags: has_tags,
+ container_repository_id: id,
+ container_repository_path: path
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb b/spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb
index 2c5c47e39c9..c58f2060001 100644
--- a/spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb
+++ b/spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb
@@ -27,8 +27,15 @@ RSpec.describe Gitlab::BackgroundMigration::SanitizeConfidentialTodos, :migratio
project_namespace_id: project_namespace2.id)
end
- let(:issue1) { issues.create!(project_id: project1.id, issue_type: 1, title: 'issue1', author_id: user.id) }
- let(:issue2) { issues.create!(project_id: project2.id, issue_type: 1, title: 'issue2') }
+ let(:issue1) do
+ issues.create!(
+ project_id: project1.id, namespace_id: project_namespace1.id, issue_type: 1, title: 'issue1', author_id: user.id
+ )
+ end
+
+ let(:issue2) do
+ issues.create!(project_id: project2.id, namespace_id: project_namespace2.id, issue_type: 1, title: 'issue2')
+ end
let(:public_note) { notes.create!(note: 'text', project_id: project1.id) }
diff --git a/spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb b/spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb
index 982e3319063..908f11aabc3 100644
--- a/spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb
+++ b/spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb
@@ -3,19 +3,19 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::UpdateTimelogsNullSpentAt, schema: 20211215090620 do
- let_it_be(:previous_time) { 10.days.ago }
- let_it_be(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
- let_it_be(:project) { table(:projects).create!(namespace_id: namespace.id) }
- let_it_be(:issue) { table(:issues).create!(project_id: project.id) }
- let_it_be(:merge_request) { table(:merge_requests).create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature') }
- let_it_be(:timelog1) { create_timelog!(issue_id: issue.id) }
- let_it_be(:timelog2) { create_timelog!(merge_request_id: merge_request.id) }
- let_it_be(:timelog3) { create_timelog!(issue_id: issue.id, spent_at: previous_time) }
- let_it_be(:timelog4) { create_timelog!(merge_request_id: merge_request.id, spent_at: previous_time) }
+ let!(:previous_time) { 10.days.ago }
+ let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
+ let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:issue) { table(:issues).create!(project_id: project.id) }
+ let!(:merge_request) { table(:merge_requests).create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature') }
+ let!(:timelog1) { create_timelog!(issue_id: issue.id) }
+ let!(:timelog2) { create_timelog!(merge_request_id: merge_request.id) }
+ let!(:timelog3) { create_timelog!(issue_id: issue.id, spent_at: previous_time) }
+ let!(:timelog4) { create_timelog!(merge_request_id: merge_request.id, spent_at: previous_time) }
subject(:background_migration) { described_class.new }
- before_all do
+ before do
table(:timelogs).where.not(id: [timelog3.id, timelog4.id]).update_all(spent_at: nil)
end
diff --git a/spec/lib/gitlab/blob_helper_spec.rb b/spec/lib/gitlab/blob_helper_spec.rb
index a2f20dcd4fc..e18277ba8d2 100644
--- a/spec/lib/gitlab/blob_helper_spec.rb
+++ b/spec/lib/gitlab/blob_helper_spec.rb
@@ -68,6 +68,7 @@ RSpec.describe Gitlab::BlobHelper do
expect(blob.image?).to be_falsey
end
end
+
context 'with a .webp file' do
it 'returns true' do
expect(webp_blob.image?).to be_truthy
diff --git a/spec/lib/gitlab/bullet_spec.rb b/spec/lib/gitlab/bullet_spec.rb
index 1262a0b8bde..c575c656bb4 100644
--- a/spec/lib/gitlab/bullet_spec.rb
+++ b/spec/lib/gitlab/bullet_spec.rb
@@ -3,48 +3,55 @@
require 'spec_helper'
RSpec.describe Gitlab::Bullet do
- describe '#enabled?' do
- it 'is enabled' do
- stub_env('ENABLE_BULLET', true)
-
- expect(described_class.enabled?).to be(true)
- end
-
- it 'is not enabled' do
+ context 'with bullet installed' do
+ before do
stub_env('ENABLE_BULLET', nil)
-
- expect(described_class.enabled?).to be(false)
+ stub_const('::Bullet', double)
end
- it 'is correctly aliased for #extra_logging_enabled?' do
- expect(described_class.method(:extra_logging_enabled?).original_name).to eq(:enabled?)
- end
- end
+ describe '#enabled?' do
+ context 'with env enabled' do
+ before do
+ stub_env('ENABLE_BULLET', true)
+ allow(Gitlab.config.bullet).to receive(:enabled).and_return(false)
+ end
- describe '#configure_bullet?' do
- context 'with ENABLE_BULLET true' do
- before do
- stub_env('ENABLE_BULLET', true)
+ it 'is enabled' do
+ expect(described_class.enabled?).to be(true)
+ end
end
- it 'is configurable' do
- expect(described_class.configure_bullet?).to be(true)
+ context 'with env disabled' do
+ before do
+ stub_env('ENABLE_BULLET', false)
+ allow(Gitlab.config.bullet).to receive(:enabled).and_return(true)
+ end
+
+ it 'is not enabled' do
+ expect(described_class.enabled?).to be(false)
+ end
end
end
- context 'with ENABLE_BULLET falsey' do
- before do
- stub_env('ENABLE_BULLET', nil)
- end
+ describe '#configure_bullet?' do
+ context 'with config enabled' do
+ before do
+ allow(Gitlab.config.bullet).to receive(:enabled).and_return(true)
+ end
- it 'is not configurable' do
- expect(described_class.configure_bullet?).to be(false)
+ it 'is configurable' do
+ expect(described_class.configure_bullet?).to be(true)
+ end
end
- it 'is configurable in development' do
- allow(Rails).to receive_message_chain(:env, :development?).and_return(true)
+ context 'with config disabled' do
+ before do
+ allow(Gitlab.config.bullet).to receive(:enabled).and_return(false)
+ end
- expect(described_class.configure_bullet?).to be(true)
+ it 'is not configurable' do
+ expect(described_class.configure_bullet?).to be(false)
+ end
end
end
end
diff --git a/spec/lib/gitlab/checks/timed_logger_spec.rb b/spec/lib/gitlab/checks/timed_logger_spec.rb
index 6c488212eca..261fdd6c002 100644
--- a/spec/lib/gitlab/checks/timed_logger_spec.rb
+++ b/spec/lib/gitlab/checks/timed_logger_spec.rb
@@ -17,38 +17,44 @@ RSpec.describe Gitlab::Checks::TimedLogger do
logger.append_message("Checking ref: #{ref}")
end
+ around do |example|
+ freeze_time do
+ example.run
+ end
+ end
+
describe '#log_timed' do
it 'logs message' do
- Timecop.freeze(start + 30.seconds) do
- logger.log_timed(log_messages[:foo], start) { bar_check }
- end
+ travel_to(start + 30.seconds)
+
+ logger.log_timed(log_messages[:foo], start) { bar_check }
expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (30000.0ms)")
end
context 'when time limit was reached' do
it 'cancels action' do
- Timecop.freeze(start + 50.seconds) do
- expect do
- logger.log_timed(log_messages[:foo], start) do
- bar_check
- end
- end.to raise_error(described_class::TimeoutError)
- end
+ travel_to(start + 50.seconds)
+
+ expect do
+ logger.log_timed(log_messages[:foo], start) do
+ bar_check
+ end
+ end.to raise_error(described_class::TimeoutError)
expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled)")
end
it 'cancels action with time elapsed if work was performed' do
- Timecop.freeze(start + 30.seconds) do
- expect do
- logger.log_timed(log_messages[:foo], start) do
- grpc_check
- end
- end.to raise_error(described_class::TimeoutError)
-
- expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled after 30000.0ms)")
- end
+ travel_to(start + 30.seconds)
+
+ expect do
+ logger.log_timed(log_messages[:foo], start) do
+ grpc_check
+ end
+ end.to raise_error(described_class::TimeoutError)
+
+ expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled after 30000.0ms)")
end
end
end
diff --git a/spec/lib/gitlab/ci/build/cache_spec.rb b/spec/lib/gitlab/ci/build/cache_spec.rb
index 7477aedb994..a8fa14b4b4c 100644
--- a/spec/lib/gitlab/ci/build/cache_spec.rb
+++ b/spec/lib/gitlab/ci/build/cache_spec.rb
@@ -14,8 +14,8 @@ RSpec.describe Gitlab::Ci::Build::Cache do
cache = described_class.new(cache_config, pipeline)
- expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, { key: 'key-a' })
- expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, { key: 'key-b' })
+ expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, { key: 'key-a' }, 0)
+ expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, { key: 'key-b' }, 1)
expect(cache.instance_variable_get(:@cache)).to eq([cache_seed_a, cache_seed_b])
end
end
@@ -29,7 +29,7 @@ RSpec.describe Gitlab::Ci::Build::Cache do
cache = described_class.new(cache_config, pipeline)
- expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, cache_config)
+ expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, cache_config, 0)
expect(cache.instance_variable_get(:@cache)).to eq([cache_seed])
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 7f862a3b80a..74739a67be0 100644
--- a/spec/lib/gitlab/ci/build/context/build_spec.rb
+++ b/spec/lib/gitlab/ci/build/context/build_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Build::Context::Build do
+RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_authoring do
let(:pipeline) { create(:ci_pipeline) }
let(:seed_attributes) { { 'name' => 'some-job' } }
- let(:context) { described_class.new(pipeline, seed_attributes) }
+ subject(:context) { described_class.new(pipeline, seed_attributes) }
shared_examples 'variables collection' do
it { is_expected.to include('CI_COMMIT_REF_NAME' => 'master') }
@@ -22,6 +22,12 @@ RSpec.describe Gitlab::Ci::Build::Context::Build do
it { is_expected.to include('CI_BUILD_REF_NAME' => 'master') }
it { is_expected.to include('CI_PROJECT_PATH' => pipeline.project.full_path) }
end
+
+ context 'when environment:name is provided' do
+ let(:seed_attributes) { { 'name' => 'some-job', 'environment' => 'test' } }
+
+ it { is_expected.to include('CI_ENVIRONMENT_NAME' => 'test') }
+ end
end
describe '#variables' do
diff --git a/spec/lib/gitlab/ci/build/hook_spec.rb b/spec/lib/gitlab/ci/build/hook_spec.rb
new file mode 100644
index 00000000000..6ed40a44c97
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/hook_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Build::Hook, feature_category: :pipeline_authoring do
+ let_it_be(:build1) do
+ FactoryBot.build(:ci_build,
+ options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } })
+ end
+
+ describe '.from_hooks' do
+ subject(:from_hooks) { described_class.from_hooks(build1) }
+
+ it 'initializes and returns hooks' do
+ expect(from_hooks.size).to eq(1)
+ expect(from_hooks[0].name).to eq('pre_get_sources_script')
+ expect(from_hooks[0].script).to eq(["echo 'hello pre_get_sources_script'"])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
index 7476fc6c25f..6264e0c8e33 100644
--- a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
@@ -142,6 +142,26 @@ RSpec.describe Gitlab::Ci::Config::Entry::Artifacts do
end
end
+ context 'when the `when` keyword is not a string' do
+ context 'when it is an array' do
+ let(:config) { { paths: %w[results.txt], when: ['always'] } }
+
+ it 'returns error' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'artifacts when should be a string'
+ end
+ end
+
+ context 'when it is a boolean' do
+ let(:config) { { paths: %w[results.txt], when: true } }
+
+ it 'returns error' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'artifacts when should be a string'
+ end
+ end
+ end
+
describe 'excluded artifacts' do
context 'when configuration is valid' do
let(:config) { { untracked: true, exclude: ['some/directory/'] } }
diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
index 8da46561b73..736c184a289 100644
--- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
# that we know that we don't want to inherit
# as they do not have sense in context of Bridge
let(:ignored_inheritable_columns) do
- %i[before_script after_script image services cache interruptible timeout
+ %i[before_script after_script hooks image services cache interruptible timeout
retry tags artifacts]
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index 247f4b63910..414cbb169b9 100644
--- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -163,22 +163,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
end
end
- context 'when policy is unknown' do
- let(:config) { { policy: 'unknown' } }
-
- it 'reports error' do
- is_expected.to include('cache policy should be pull-push, push, or pull')
- end
- end
-
- context 'when `when` is unknown' do
- let(:config) { { when: 'unknown' } }
-
- it 'reports error' do
- is_expected.to include('cache when should be on_success, on_failure or always')
- end
- end
-
context 'when descendants are invalid' do
context 'with invalid keys' do
let(:config) { { key: 1 } }
@@ -228,6 +212,62 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
is_expected.to include 'cache config contains unknown keys: invalid'
end
end
+
+ context 'when the `when` keyword is not a valid string' do
+ context 'when `when` is unknown' do
+ let(:config) { { when: 'unknown' } }
+
+ it 'returns error' do
+ is_expected.to include('cache when should be one of: on_success, on_failure, always')
+ end
+ end
+
+ context 'when it is an array' do
+ let(:config) { { when: ['always'] } }
+
+ it 'returns error' do
+ expect(entry).not_to be_valid
+ is_expected.to include('cache when should be a string')
+ end
+ end
+
+ context 'when it is a boolean' do
+ let(:config) { { when: true } }
+
+ it 'returns error' do
+ expect(entry).not_to be_valid
+ is_expected.to include('cache when should be a string')
+ end
+ end
+ end
+
+ context 'when the `policy` keyword is not a valid string' do
+ context 'when `policy` is unknown' do
+ let(:config) { { policy: 'unknown' } }
+
+ it 'returns error' do
+ is_expected.to include('cache policy should be one of: pull-push, push, pull')
+ end
+ end
+
+ context 'when it is an array' do
+ let(:config) { { policy: ['pull-push'] } }
+
+ it 'returns error' do
+ expect(entry).not_to be_valid
+ is_expected.to include('cache policy should be a string')
+ end
+ end
+
+ context 'when it is a boolean' do
+ let(:config) { { policy: true } }
+
+ it 'returns error' do
+ expect(entry).not_to be_valid
+ is_expected.to include('cache policy should be a string')
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/default_spec.rb b/spec/lib/gitlab/ci/config/entry/default_spec.rb
index 5613b0f09d1..46e96843ee3 100644
--- a/spec/lib/gitlab/ci/config/entry/default_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/default_spec.rb
@@ -26,9 +26,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Default do
context 'when filtering all the entry/node names' do
it 'contains the expected node names' do
expect(described_class.nodes.keys)
- .to match_array(%i[before_script image services
- after_script cache interruptible
- timeout retry tags artifacts])
+ .to match_array(%i[before_script after_script hooks cache image services
+ interruptible timeout retry tags artifacts])
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/hooks_spec.rb b/spec/lib/gitlab/ci/config/entry/hooks_spec.rb
new file mode 100644
index 00000000000..7a5ff244e18
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/hooks_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+RSpec.describe Gitlab::Ci::Config::Entry::Hooks do
+ subject(:entry) { described_class.new(config) }
+
+ before do
+ entry.compose!
+ end
+
+ describe 'validations' do
+ context 'when passing a valid hook' do
+ let(:config) { { pre_get_sources_script: ['ls'] } }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when passing an invalid hook' do
+ let(:config) { { x_get_something: ['ls'] } }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when entry config is not a hash' do
+ let(:config) { 'ls' }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe '#value' do
+ let(:config) { { pre_get_sources_script: ['ls'] } }
+
+ it 'returns a hash' do
+ expect(entry.value).to eq(config)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/id_token_spec.rb b/spec/lib/gitlab/ci/config/entry/id_token_spec.rb
new file mode 100644
index 00000000000..12585d662ec
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/id_token_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Entry::IdToken do
+ context 'when given `aud` as a string' do
+ it 'is valid' do
+ config = { aud: 'https://gitlab.com' }
+ id_token = described_class.new(config)
+
+ id_token.compose!
+
+ expect(id_token).to be_valid
+ expect(id_token.value).to eq(aud: 'https://gitlab.com')
+ end
+ end
+
+ context 'when given `aud` as an array' do
+ it 'is valid and concatenates the values' do
+ config = { aud: ['https://gitlab.com', 'https://aws.com'] }
+ id_token = described_class.new(config)
+
+ id_token.compose!
+
+ expect(id_token).to be_valid
+ expect(id_token.value).to eq(aud: ['https://gitlab.com', 'https://aws.com'])
+ end
+ end
+
+ context 'when not given an `aud`' do
+ it 'is invalid' do
+ config = {}
+ id_token = described_class.new(config)
+
+ id_token.compose!
+
+ expect(id_token).not_to be_valid
+ expect(id_token.errors).to match_array([
+ 'id token config missing required keys: aud',
+ 'id token aud should be an array of strings or a string'
+ ])
+ end
+ end
+
+ context 'when given an unknown keyword' do
+ it 'is invalid' do
+ config = { aud: 'https://gitlab.com', unknown: 'test' }
+ id_token = described_class.new(config)
+
+ id_token.compose!
+
+ expect(id_token).not_to be_valid
+ expect(id_token.errors).to match_array([
+ 'id token config contains unknown keys: unknown'
+ ])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index acf60a6cdda..becb46ac2e7 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
subject { described_class.nodes.keys }
let(:result) do
- %i[before_script script stage after_script cache
+ %i[before_script script after_script hooks stage cache
image services only except rules needs variables artifacts
environment coverage retry interruptible timeout release tags
inherit parallel]
@@ -716,7 +716,9 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
let(:config) do
{ before_script: %w[ls pwd],
script: 'rspec',
- after_script: %w[cleanup] }
+ after_script: %w[cleanup],
+ id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } },
+ hooks: { pre_get_sources_script: 'echo hello' } }
end
it 'returns correct value' do
@@ -727,10 +729,33 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
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)
+ scheduling_type: :stage,
+ id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } })
+ end
+
+ context 'when the FF ci_hooks_pre_get_sources_script is disabled' do
+ before do
+ stub_feature_flags(ci_hooks_pre_get_sources_script: false)
+ end
+
+ it 'returns correct value' do
+ expect(entry.value)
+ .to eq(name: :rspec,
+ before_script: %w[ls pwd],
+ script: %w[rspec],
+ stage: 'test',
+ ignore: false,
+ after_script: %w[cleanup],
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage,
+ id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } })
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 085293d7368..c40589104cd 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Root do
let(:user) {}
let(:project) {}
- let(:root) { described_class.new(hash, user: user, project: project) }
+ let(:logger) { Gitlab::Ci::Pipeline::Logger.new(project: project) }
+ let(:root) { described_class.new(hash, user: user, project: project, logger: logger) }
describe '.nodes' do
it 'returns a hash' do
@@ -37,7 +38,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
variables: {
VAR: 'root',
VAR2: { value: 'val 2', description: 'this is var 2' },
- VAR3: { value: %w[val3 val3b], description: 'this is var 3' }
+ 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),
@@ -228,6 +229,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
)
end
end
+
+ it 'tracks log entries' do
+ expect(logger.observations_hash).to match(
+ a_hash_including(
+ 'config_root_compose_jobs_factory_duration_s' => a_kind_of(Numeric)
+ )
+ )
+ end
end
end
@@ -317,6 +326,42 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
end
end
+ context 'when variables have `options` data' do
+ before do
+ root.compose!
+ end
+
+ context 'and the value is in the `options` array' do
+ let(:hash) do
+ {
+ variables: { 'VAR' => { value: 'val1', options: %w[val1 val2] } },
+ rspec: { script: 'bin/rspec' }
+ }
+ end
+
+ it 'returns correct value' do
+ expect(root.variables_entry.value_with_data).to eq(
+ 'VAR' => { value: 'val1' }
+ )
+
+ expect(root.variables_value).to eq('VAR' => 'val1')
+ end
+ end
+
+ context 'and the value is not in the `options` array' do
+ let(:hash) do
+ {
+ variables: { 'VAR' => { value: 'val', options: %w[val1 val2] } },
+ rspec: { script: 'bin/rspec' }
+ }
+ end
+
+ it 'returns an error' do
+ expect(root.errors).to contain_exactly('variables:var config value must be present in options')
+ end
+ end
+ end
+
context 'when variables have "expand" data' do
let(:hash) do
{
diff --git a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
index d0116c961d7..f47923af45a 100644
--- a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::Entry::Trigger do
+RSpec.describe Gitlab::Ci::Config::Entry::Trigger, feature_category: :pipeline_authoring do
subject { described_class.new(config) }
context 'when trigger config is a non-empty string' do
@@ -35,6 +35,48 @@ RSpec.describe Gitlab::Ci::Config::Entry::Trigger do
end
context 'when trigger is a hash - cross-project' do
+ context 'when project is a string' do
+ context 'when project is a non-empty string' do
+ let(:config) { { project: 'some/project' } }
+
+ it 'is valid' do
+ expect(subject).to be_valid
+ end
+ end
+
+ context 'when project is an empty string' do
+ let(:config) { { project: '' } }
+
+ it 'returns error' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.first)
+ .to match /project can't be blank/
+ end
+ end
+ end
+
+ context 'when project is not a string' do
+ context 'when project is an array' do
+ let(:config) { { project: ['some/project'] } }
+
+ it 'returns error' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.first)
+ .to match /should be a string/
+ end
+ end
+
+ context 'when project is a boolean' do
+ let(:config) { { project: true } }
+
+ it 'returns error' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.first)
+ .to match /should be a string/
+ end
+ end
+ end
+
context 'when branch is provided' do
let(:config) { { project: 'some/project', branch: 'feature' } }
diff --git a/spec/lib/gitlab/ci/config/entry/variable_spec.rb b/spec/lib/gitlab/ci/config/entry/variable_spec.rb
index d7023072312..97b06c8b1a5 100644
--- a/spec/lib/gitlab/ci/config/entry/variable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/variable_spec.rb
@@ -306,48 +306,48 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variable do
end
end
end
- end
- describe 'ComplexArrayVariable' do
- context 'when allow_array_value metadata is false' do
- let(:config) { { value: %w[value value2], description: 'description' } }
- let(:metadata) { { allow_array_value: false } }
+ context 'when config is a hash with options' do
+ context 'when there is no metadata' do
+ let(:config) { { value: 'value', options: %w[value value2] } }
+ let(:metadata) { {} }
- describe '#valid?' do
- it { is_expected.not_to be_valid }
- end
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
- describe '#errors' do
- subject(:errors) { entry.errors }
+ describe '#errors' do
+ subject(:errors) { entry.errors }
- it { is_expected.to include 'var1 config value must be an alphanumeric string' }
+ it { is_expected.to include 'var1 config must be a string' }
+ end
end
- end
- context 'when allow_array_value metadata is true' do
- let(:config) { { value: %w[value value2], description: 'description' } }
- let(:metadata) { { allowed_value_data: %i[value description], allow_array_value: true } }
+ context 'when options are allowed' do
+ let(:config) { { value: 'value', options: %w[value value2] } }
+ let(:metadata) { { allowed_value_data: %i[value options] } }
- describe '#valid?' do
- it { is_expected.to be_valid }
- end
+ describe '#valid?' do
+ it { is_expected.to be_valid }
+ end
- describe '#value' do
- subject(:value) { entry.value }
+ describe '#value' do
+ subject(:value) { entry.value }
- it { is_expected.to eq('value') }
- end
+ it { is_expected.to eq('value') }
+ end
- describe '#value_with_data' do
- subject(:value_with_data) { entry.value_with_data }
+ describe '#value_with_data' do
+ subject(:value_with_data) { entry.value_with_data }
- it { is_expected.to eq(value: 'value') }
- end
+ it { is_expected.to eq(value: 'value') }
+ end
- describe '#value_with_prefill_data' do
- subject(:value_with_prefill_data) { entry.value_with_prefill_data }
+ describe '#value_with_prefill_data' do
+ subject(:value_with_prefill_data) { entry.value_with_prefill_data }
- it { is_expected.to eq(value: 'value', description: 'description', value_options: %w[value value2]) }
+ it { is_expected.to eq(value: 'value', options: %w[value value2]) }
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
index 609e4422d5c..e7dbc78729d 100644
--- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
@@ -116,8 +116,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
it_behaves_like 'invalid config', /variable_1 config must be a string/
end
- context 'when metadata has allow_array_value and allowed_value_data' do
- let(:metadata) { { allowed_value_data: %i[value description], allow_array_value: true } }
+ context 'when metadata has the allowed_value_data key' do
+ let(:metadata) { { allowed_value_data: %i[value description options] } }
let(:result) do
{ 'VARIABLE_1' => 'value' }
@@ -143,17 +143,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
end
end
- context 'when entry config value has key-value pair and value is an array' do
+ context 'when entry config value has options' do
let(:config) do
- { 'VARIABLE_1' => { value: %w[value1 value2], description: 'variable 1' } }
+ { 'VARIABLE_1' => {
+ value: 'value1', options: %w[value1 value2], description: 'variable 1'
+ } }
end
- context 'when there is no allowed_value_data metadata' do
- it_behaves_like 'invalid config', /variable_1 config value must be an alphanumeric string/
- end
-
- context 'when metadata has allow_array_value and allowed_value_data' do
- let(:metadata) { { allowed_value_data: %i[value description], allow_array_value: true } }
+ context 'when metadata has allowed_value_data' do
+ let(:metadata) { { allowed_value_data: %i[value description options] } }
let(:result) do
{ 'VARIABLE_1' => 'value1' }
@@ -172,7 +170,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
describe '#value_with_prefill_data' do
it 'returns variable with prefill data' do
expect(entry.value_with_prefill_data).to eq(
- 'VARIABLE_1' => { value: 'value1', value_options: %w[value1 value2], description: 'variable 1' }
+ 'VARIABLE_1' => { value: 'value1', options: %w[value1 value2], description: 'variable 1' }
)
end
end
@@ -234,14 +232,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
it_behaves_like 'invalid config', /variable_1 config uses invalid data keys: hello/
end
- context 'when entry config value has hash with nil description' do
- let(:config) do
- { 'VARIABLE_1' => { value: 'value 1', description: nil } }
- end
-
- it_behaves_like 'invalid config', /variable_1 config description must be an alphanumeric string/
- end
-
context 'when entry config value has hash without description' do
let(:config) do
{ 'VARIABLE_1' => { value: 'value 1' } }
diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
index c22afb32756..8d93cdcf378 100644
--- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
@@ -188,6 +188,19 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote do
'is blocked: Requests to localhost are not allowed!'
end
end
+
+ context 'when connection refused error has been raised' do
+ let(:location) { 'http://127.0.0.1/some/path/to/config.yaml' }
+ let(:exception) { Errno::ECONNREFUSED.new }
+
+ before do
+ stub_full_request(location).to_raise(exception)
+ end
+
+ it 'returns details about connection failure' do
+ expect(subject).to eq "Remote file could not be fetched because Connection refused!"
+ end
+ end
end
describe '#expand_context' do
diff --git a/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb
new file mode 100644
index 00000000000..0fdcc5e8ff7
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::External::Mapper::Base, feature_category: :pipeline_authoring do
+ let(:test_class) do
+ Class.new(described_class) do
+ def self.name
+ 'TestClass'
+ end
+ end
+ end
+
+ let(:context) { Gitlab::Ci::Config::External::Context.new }
+ let(:mapper) { test_class.new(context) }
+
+ describe '#process' do
+ subject(:process) { mapper.process }
+
+ context 'when the method is not implemented' do
+ it 'raises NotImplementedError' do
+ expect { process }.to raise_error(NotImplementedError)
+ end
+ end
+
+ context 'when the method is implemented' do
+ before do
+ test_class.class_eval do
+ def process_without_instrumentation
+ 'test'
+ end
+ end
+ end
+
+ it 'calls the method' do
+ expect(process).to eq('test')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb
new file mode 100644
index 00000000000..df2a2f0fd01
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::External::Mapper::Filter, feature_category: :pipeline_authoring do
+ let_it_be(:variables) do
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'VARIABLE1', value: 'hello')
+ end
+ end
+
+ let_it_be(:context) do
+ Gitlab::Ci::Config::External::Context.new(variables: variables)
+ end
+
+ subject(:filter) { described_class.new(context) }
+
+ describe '#process' do
+ let(:locations) do
+ [{ local: 'config/.gitlab-ci.yml', rules: [{ if: '$VARIABLE1' }] },
+ { remote: 'https://example.com/.gitlab-ci.yml', rules: [{ if: '$VARIABLE2' }] }]
+ end
+
+ subject(:process) { filter.process(locations) }
+
+ it 'filters locations according to rules' do
+ is_expected.to eq(
+ [{ local: 'config/.gitlab-ci.yml', rules: [{ if: '$VARIABLE1' }] }]
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb
new file mode 100644
index 00000000000..b14b6b0ca29
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::External::Mapper::LocationExpander, feature_category: :pipeline_authoring do
+ include RepoHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.owner }
+
+ let(:sha) { project.commit.sha }
+
+ let(:context) do
+ Gitlab::Ci::Config::External::Context.new(project: project, user: user, sha: sha)
+ end
+
+ subject(:location_expander) { described_class.new(context) }
+
+ describe '#process' do
+ subject(:process) { location_expander.process(locations) }
+
+ context 'when there are project files' do
+ let(:locations) do
+ [{ project: 'gitlab-org/gitlab-1', file: ['builds.yml', 'tests.yml'] },
+ { project: 'gitlab-org/gitlab-2', file: 'deploy.yml' }]
+ end
+
+ it 'returns expanded locations' do
+ is_expected.to eq(
+ [{ project: 'gitlab-org/gitlab-1', file: 'builds.yml' },
+ { project: 'gitlab-org/gitlab-1', file: 'tests.yml' },
+ { project: 'gitlab-org/gitlab-2', file: 'deploy.yml' }]
+ )
+ end
+ end
+
+ context 'when there are local files' do
+ let(:locations) do
+ [{ local: 'builds/*.yml' },
+ { local: 'tests.yml' }]
+ end
+
+ let(:project_files) do
+ { 'builds/1.yml' => 'a', 'builds/2.yml' => 'b', 'tests.yml' => 'c' }
+ end
+
+ around do |example|
+ create_and_delete_files(project, project_files) do
+ example.run
+ end
+ end
+
+ it 'returns expanded locations' do
+ is_expected.to eq(
+ [{ local: 'builds/1.yml' },
+ { local: 'builds/2.yml' },
+ { local: 'tests.yml' }]
+ )
+ end
+ end
+
+ context 'when there are other files' do
+ let(:locations) do
+ [{ remote: 'https://gitlab.com/gitlab-org/gitlab-ce/raw/master/.gitlab-ci.yml' }]
+ end
+
+ it 'returns the same location' do
+ is_expected.to eq(locations)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb
new file mode 100644
index 00000000000..5f321a696c9
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: :pipeline_authoring do
+ let_it_be(:variables) do
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'A_MASKED_VAR', value: 'this-is-secret', masked: true)
+ end
+ end
+
+ let_it_be(:context) do
+ Gitlab::Ci::Config::External::Context.new(variables: variables)
+ end
+
+ subject(:matcher) { described_class.new(context) }
+
+ describe '#process' do
+ let(:locations) do
+ [{ local: 'file.yml' },
+ { file: 'file.yml', project: 'namespace/project' },
+ { remote: 'https://example.com/.gitlab-ci.yml' },
+ { template: 'file.yml' },
+ { artifact: 'generated.yml', job: 'test' }]
+ end
+
+ subject(:process) { matcher.process(locations) }
+
+ it 'returns an array of file objects' do
+ is_expected.to contain_exactly(
+ an_instance_of(Gitlab::Ci::Config::External::File::Local),
+ an_instance_of(Gitlab::Ci::Config::External::File::Project),
+ an_instance_of(Gitlab::Ci::Config::External::File::Remote),
+ an_instance_of(Gitlab::Ci::Config::External::File::Template),
+ an_instance_of(Gitlab::Ci::Config::External::File::Artifact)
+ )
+ end
+
+ context 'when a location is not valid' do
+ let(:locations) { [{ invalid: 'file.yml' }] }
+
+ it 'raises an error' do
+ expect { process }.to raise_error(
+ Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError,
+ '`{"invalid":"file.yml"}` does not have a valid subkey for include. ' \
+ 'Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`'
+ )
+ end
+
+ context 'when the invalid location includes a masked variable' do
+ let(:locations) { [{ invalid: 'this-is-secret.yml' }] }
+
+ it 'raises an error with a masked sentence' do
+ expect { process }.to raise_error(
+ Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError,
+ '`{"invalid":"xxxxxxxxxxxxxx.yml"}` does not have a valid subkey for include. ' \
+ 'Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`'
+ )
+ end
+ end
+ end
+
+ context 'when a location is ambiguous' do
+ let(:locations) { [{ local: 'file.yml', remote: 'https://example.com/.gitlab-ci.yml' }] }
+
+ it 'raises an error' do
+ expect { process }.to raise_error(
+ Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError,
+ "Each include must use only one of: `local`, `project`, `remote`, `template`, `artifact`"
+ )
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb
new file mode 100644
index 00000000000..709c234253b
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::External::Mapper::Normalizer, feature_category: :pipeline_authoring do
+ let_it_be(:variables) do
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'VARIABLE1', value: 'config')
+ variables.append(key: 'VARIABLE2', value: 'https://example.com')
+ end
+ end
+
+ let_it_be(:context) do
+ Gitlab::Ci::Config::External::Context.new(variables: variables)
+ end
+
+ subject(:normalizer) { described_class.new(context) }
+
+ describe '#process' do
+ let(:locations) do
+ ['https://example.com/.gitlab-ci.yml',
+ 'config/.gitlab-ci.yml',
+ { local: 'config/.gitlab-ci.yml' },
+ { remote: 'https://example.com/.gitlab-ci.yml' },
+ { template: 'Template.gitlab-ci.yml' },
+ '$VARIABLE1/.gitlab-ci.yml',
+ '$VARIABLE2/.gitlab-ci.yml']
+ end
+
+ subject(:process) { normalizer.process(locations) }
+
+ it 'converts locations to canonical form' do
+ is_expected.to eq(
+ [{ remote: 'https://example.com/.gitlab-ci.yml' },
+ { local: 'config/.gitlab-ci.yml' },
+ { local: 'config/.gitlab-ci.yml' },
+ { remote: 'https://example.com/.gitlab-ci.yml' },
+ { template: 'Template.gitlab-ci.yml' },
+ { local: 'config/.gitlab-ci.yml' },
+ { remote: 'https://example.com/.gitlab-ci.yml' }]
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb
new file mode 100644
index 00000000000..f7454dcd4be
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::External::Mapper::VariablesExpander, feature_category: :pipeline_authoring do
+ let_it_be(:variables) do
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'VARIABLE1', value: 'hello')
+ end
+ end
+
+ let_it_be(:context) do
+ Gitlab::Ci::Config::External::Context.new(variables: variables)
+ end
+
+ subject(:variables_expander) { described_class.new(context) }
+
+ describe '#process' do
+ subject(:process) { variables_expander.process(locations) }
+
+ context 'when locations are strings' do
+ let(:locations) { ['$VARIABLE1.gitlab-ci.yml'] }
+
+ it 'expands variables' do
+ is_expected.to eq(['hello.gitlab-ci.yml'])
+ end
+ end
+
+ context 'when locations are hashes' do
+ let(:locations) { [{ local: '$VARIABLE1.gitlab-ci.yml' }] }
+
+ it 'expands variables' do
+ is_expected.to eq([{ local: 'hello.gitlab-ci.yml' }])
+ end
+ end
+
+ context 'when locations are arrays' do
+ let(:locations) { [{ local: ['$VARIABLE1.gitlab-ci.yml'] }] }
+
+ it 'expands variables' do
+ is_expected.to eq([{ local: ['hello.gitlab-ci.yml'] }])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb
new file mode 100644
index 00000000000..7c7252c6b0e
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: :pipeline_authoring do
+ include RepoHelpers
+ include StubRequests
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.owner }
+
+ let(:context) do
+ Gitlab::Ci::Config::External::Context.new(project: project, user: user, sha: project.commit.id)
+ end
+
+ let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
+
+ let(:project_files) do
+ {
+ 'myfolder/file1.yml' => <<~YAML,
+ my_build:
+ script: echo Hello World
+ YAML
+ 'myfolder/file2.yml' => <<~YAML,
+ my_test:
+ script: echo Hello World
+ YAML
+ 'nested_configs.yml' => <<~YAML
+ include:
+ - local: myfolder/file1.yml
+ - local: myfolder/file2.yml
+ - remote: #{remote_url}
+ YAML
+ }
+ end
+
+ around(:all) do |example|
+ create_and_delete_files(project, project_files) do
+ example.run
+ end
+ end
+
+ before do
+ stub_full_request(remote_url).to_return(
+ body: <<~YAML
+ remote_test:
+ script: echo Hello World
+ YAML
+ )
+ end
+
+ subject(:verifier) { described_class.new(context) }
+
+ describe '#process' do
+ subject(:process) { verifier.process(files) }
+
+ context 'when files are local' do
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context),
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context)
+ ]
+ end
+
+ it 'returns an array of file objects' do
+ expect(process.map(&:location)).to contain_exactly('myfolder/file1.yml', 'myfolder/file2.yml')
+ end
+
+ it 'adds files to the expandset' do
+ expect { process }.to change { context.expandset.count }.by(2)
+ end
+ end
+
+ context 'when a file includes other files' do
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'nested_configs.yml' }, context)
+ ]
+ end
+
+ it 'returns an array of file objects with combined hash' do
+ expect(process.map(&:to_hash)).to contain_exactly(
+ { my_build: { script: 'echo Hello World' },
+ my_test: { script: 'echo Hello World' },
+ remote_test: { script: 'echo Hello World' } }
+ )
+ end
+ end
+
+ context 'when there is an invalid file' do
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/invalid.yml' }, context)
+ ]
+ end
+
+ it 'adds an error to the file' do
+ expect(process.first.errors).to include("Local file `myfolder/invalid.yml` does not exist!")
+ end
+ end
+
+ context 'when max_includes is exceeded' do
+ context 'when files are nested' do
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'nested_configs.yml' }, context)
+ ]
+ end
+
+ before do
+ allow(context).to receive(:max_includes).and_return(1)
+ end
+
+ it 'raises Processor::IncludeError' do
+ expect { process }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError)
+ end
+ end
+
+ context 'when files are not nested' do
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context),
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context)
+ ]
+ end
+
+ before do
+ allow(context).to receive(:max_includes).and_return(1)
+ end
+
+ it 'raises Mapper::TooManyIncludesError' do
+ expect { process }.to raise_error(Gitlab::Ci::Config::External::Mapper::TooManyIncludesError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index d905568f01e..b7e58d4dfa1 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -2,8 +2,10 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Mapper do
+# This will be removed with FF ci_refactoring_external_mapper and moved to below.
+RSpec.shared_context 'gitlab_ci_config_external_mapper' do
include StubRequests
+ include RepoHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.owner }
@@ -12,13 +14,13 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:template_file) { 'Auto-DevOps.gitlab-ci.yml' }
let(:variables) { project.predefined_variables }
- let(:context_params) { { project: project, sha: '123456', user: user, variables: variables } }
+ let(:context_params) { { project: project, sha: project.commit.sha, user: user, variables: variables } }
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:file_content) do
- <<~HEREDOC
+ <<~YAML
image: 'image:1.0'
- HEREDOC
+ YAML
end
subject(:mapper) { described_class.new(values, context) }
@@ -38,7 +40,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
it 'propagates the pipeline logger' do
process
- fetch_content_log_count = mapper
+ fetch_content_log_count = context
.logger
.observations_hash
.dig(key, 'count')
@@ -231,7 +233,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
it 'has expanset with one' do
process
- expect(mapper.expandset.size).to eq(1)
+ expect(context.expandset.size).to eq(1)
end
end
@@ -379,17 +381,28 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
end
context 'when local file path has wildcard' do
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
let(:values) do
{ include: 'myfolder/*.yml' }
end
- before do
- allow_next_instance_of(Repository) do |repository|
- allow(repository).to receive(:search_files_by_wildcard_path).with('myfolder/*.yml', '123456') do
- ['myfolder/file1.yml', 'myfolder/file2.yml']
- end
+ let(:project_files) do
+ {
+ 'myfolder/file1.yml' => <<~YAML,
+ my_build:
+ script: echo Hello World
+ YAML
+ 'myfolder/file2.yml' => <<~YAML
+ my_test:
+ script: echo Hello World
+ YAML
+ }
+ end
+
+ around do |example|
+ create_and_delete_files(project, project_files) do
+ example.run
end
end
@@ -445,8 +458,20 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
it 'has expanset with two' do
process
- expect(mapper.expandset.size).to eq(2)
+ expect(context.expandset.size).to eq(2)
end
end
end
end
+
+RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline_authoring do
+ it_behaves_like 'gitlab_ci_config_external_mapper'
+
+ context 'when the FF ci_refactoring_external_mapper is disabled' do
+ before do
+ stub_feature_flags(ci_refactoring_external_mapper: false)
+ end
+
+ it_behaves_like 'gitlab_ci_config_external_mapper'
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index b1dff6f9723..c9efaf2e1af 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -2,17 +2,31 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Processor do
+RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipeline_authoring do
include StubRequests
+ include RepoHelpers
- let_it_be(:project) { create(:project, :repository) }
- let_it_be_with_reload(:another_project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
- let(:sha) { '12345' }
+ let_it_be_with_reload(:project) { create(:project, :repository) }
+ let_it_be_with_reload(:another_project) { create(:project, :repository) }
+
+ let(:project_files) { {} }
+ let(:other_project_files) { {} }
+
+ let(:sha) { project.commit.sha }
let(:context_params) { { project: project, sha: sha, user: user } }
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
- let(:processor) { described_class.new(values, context) }
+
+ subject(:processor) { described_class.new(values, context) }
+
+ around do |example|
+ create_and_delete_files(project, project_files) do
+ create_and_delete_files(another_project, other_project_files) do
+ example.run
+ end
+ end
+ end
before do
project.add_developer(user)
@@ -63,7 +77,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:values) { { include: remote_file, image: 'image:1.0' } }
let(:external_file_content) do
- <<-HEREDOC
+ <<-YAML
before_script:
- apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
- ruby -v
@@ -77,7 +91,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
rubocop:
script:
- bundle exec rubocop
- HEREDOC
+ YAML
end
before do
@@ -98,7 +112,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:values) { { include: remote_file, image: 'image:1.0' } }
let(:external_file_content) do
- <<-HEREDOC
+ <<-YAML
include:
- local: another-file.yml
rules:
@@ -107,7 +121,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
rspec:
script:
- bundle exec rspec
- HEREDOC
+ YAML
end
before do
@@ -127,19 +141,16 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
context 'with a valid local external file is defined' do
let(:values) { { include: '/lib/gitlab/ci/templates/template.yml', image: 'image:1.0' } }
let(:local_file_content) do
- <<-HEREDOC
+ <<-YAML
before_script:
- apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
- ruby -v
- which ruby
- bundle install --jobs $(nproc) "${FLAGS[@]}"
- HEREDOC
+ YAML
end
- before do
- allow_any_instance_of(Gitlab::Ci::Config::External::File::Local)
- .to receive(:fetch_local_content).and_return(local_file_content)
- end
+ let(:project_files) { { '/lib/gitlab/ci/templates/template.yml' => local_file_content } }
it 'appends the file to the values' do
output = processor.perform
@@ -153,6 +164,11 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
context 'with multiple external files are defined' do
let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
+
+ let(:local_file_content) do
+ File.read(Rails.root.join('spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml'))
+ end
+
let(:external_files) do
[
'/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml',
@@ -168,20 +184,21 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
end
let(:remote_file_content) do
- <<-HEREDOC
+ <<-YAML
stages:
- build
- review
- cleanup
- HEREDOC
+ YAML
end
- before do
- local_file_content = File.read(Rails.root.join('spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml'))
-
- allow_any_instance_of(Gitlab::Ci::Config::External::File::Local)
- .to receive(:fetch_local_content).and_return(local_file_content)
+ let(:project_files) do
+ {
+ '/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml' => local_file_content
+ }
+ end
+ before do
stub_full_request(remote_file).to_return(body: remote_file_content)
end
@@ -199,10 +216,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
let(:local_file_content) { 'invalid content file ////' }
- before do
- allow_any_instance_of(Gitlab::Ci::Config::External::File::Local)
- .to receive(:fetch_local_content).and_return(local_file_content)
- end
+ let(:project_files) { { '/lib/gitlab/ci/templates/template.yml' => local_file_content } }
it 'raises an error' do
expect { processor.perform }.to raise_error(
@@ -222,9 +236,9 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
end
let(:remote_file_content) do
- <<~HEREDOC
+ <<~YAML
image: php:5-fpm-alpine
- HEREDOC
+ YAML
end
it 'takes precedence' do
@@ -244,31 +258,32 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
}
end
- before do
- allow(project.repository).to receive(:blob_data_at).with('12345', '/local/file.yml') do
- <<~HEREDOC
- include:
- - template: Ruby.gitlab-ci.yml
- - remote: http://my.domain.com/config.yml
- - project: #{another_project.full_path}
- file: /templates/my-workflow.yml
- HEREDOC
- end
-
- allow_any_instance_of(Repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-workflow.yml') do
- <<~HEREDOC
- include:
- - local: /templates/my-build.yml
- HEREDOC
- end
+ let(:project_files) do
+ {
+ '/local/file.yml' => <<~YAML
+ include:
+ - template: Ruby.gitlab-ci.yml
+ - remote: http://my.domain.com/config.yml
+ - project: #{another_project.full_path}
+ file: /templates/my-workflow.yml
+ YAML
+ }
+ end
- allow_any_instance_of(Repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-build.yml') do
- <<~HEREDOC
- my_build:
- script: echo Hello World
- HEREDOC
- end
+ let(:other_project_files) do
+ {
+ '/templates/my-workflow.yml' => <<~YAML,
+ include:
+ - local: /templates/my-build.yml
+ YAML
+ '/templates/my-build.yml' => <<~YAML
+ my_build:
+ script: echo Hello World
+ YAML
+ }
+ end
+ before do
stub_full_request('http://my.domain.com/config.yml')
.to_return(body: 'remote_build: { script: echo Hello World }')
end
@@ -299,32 +314,32 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
expect(context.includes).to contain_exactly(
{ type: :local,
location: '/local/file.yml',
- blob: "http://localhost/#{project.full_path}/-/blob/12345/local/file.yml",
- raw: "http://localhost/#{project.full_path}/-/raw/12345/local/file.yml",
+ blob: "http://localhost/#{project.full_path}/-/blob/#{sha}/local/file.yml",
+ raw: "http://localhost/#{project.full_path}/-/raw/#{sha}/local/file.yml",
extra: {},
context_project: project.full_path,
- context_sha: '12345' },
+ context_sha: sha },
{ type: :template,
location: 'Ruby.gitlab-ci.yml',
blob: nil,
raw: 'https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml',
extra: {},
context_project: project.full_path,
- context_sha: '12345' },
+ context_sha: sha },
{ type: :remote,
location: 'http://my.domain.com/config.yml',
blob: nil,
raw: "http://my.domain.com/config.yml",
extra: {},
context_project: project.full_path,
- context_sha: '12345' },
+ context_sha: sha },
{ type: :file,
location: '/templates/my-workflow.yml',
blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-workflow.yml",
raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-workflow.yml",
extra: { project: another_project.full_path, ref: 'HEAD' },
context_project: project.full_path,
- context_sha: '12345' },
+ context_sha: sha },
{ type: :local,
location: '/templates/my-build.yml',
blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-build.yml",
@@ -393,17 +408,17 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
}
end
+ let(:other_project_files) do
+ {
+ '/templates/my-build.yml' => <<~YAML
+ my_build:
+ script: echo Hello World
+ YAML
+ }
+ end
+
before do
another_project.add_developer(user)
-
- allow_next_instance_of(Repository) do |repository|
- allow(repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-build.yml') do
- <<~HEREDOC
- my_build:
- script: echo Hello World
- HEREDOC
- end
- end
end
it 'appends the file to the values' do
@@ -423,24 +438,21 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
}
end
+ let(:other_project_files) do
+ {
+ '/templates/my-build.yml' => <<~YAML,
+ my_build:
+ script: echo Hello World
+ YAML
+ '/templates/my-test.yml' => <<~YAML
+ my_test:
+ script: echo Hello World
+ YAML
+ }
+ end
+
before do
another_project.add_developer(user)
-
- allow_next_instance_of(Repository) do |repository|
- allow(repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-build.yml') do
- <<~HEREDOC
- my_build:
- script: echo Hello World
- HEREDOC
- end
-
- allow(repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-test.yml') do
- <<~HEREDOC
- my_test:
- script: echo Hello World
- HEREDOC
- end
- end
end
it 'appends the file to the values' do
@@ -458,45 +470,34 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-build.yml",
extra: { project: another_project.full_path, ref: 'HEAD' },
context_project: project.full_path,
- context_sha: '12345' },
+ context_sha: sha },
{ type: :file,
blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-test.yml",
raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-test.yml",
location: '/templates/my-test.yml',
extra: { project: another_project.full_path, ref: 'HEAD' },
context_project: project.full_path,
- context_sha: '12345' }
+ context_sha: sha }
)
end
end
context 'when local file path has wildcard' do
- let(:project) { create(:project, :repository) }
-
let(:values) do
{ include: 'myfolder/*.yml', image: 'image:1.0' }
end
- before do
- allow_next_instance_of(Repository) do |repository|
- allow(repository).to receive(:search_files_by_wildcard_path).with('myfolder/*.yml', sha) do
- ['myfolder/file1.yml', 'myfolder/file2.yml']
- end
-
- allow(repository).to receive(:blob_data_at).with(sha, 'myfolder/file1.yml') do
- <<~HEREDOC
- my_build:
- script: echo Hello World
- HEREDOC
- end
-
- allow(repository).to receive(:blob_data_at).with(sha, 'myfolder/file2.yml') do
- <<~HEREDOC
- my_test:
- script: echo Hello World
- HEREDOC
- end
- end
+ let(:project_files) do
+ {
+ 'myfolder/file1.yml' => <<~YAML,
+ my_build:
+ script: echo Hello World
+ YAML
+ 'myfolder/file2.yml' => <<~YAML
+ my_test:
+ script: echo Hello World
+ YAML
+ }
end
it 'fetches the matched files' do
@@ -510,18 +511,18 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
expect(context.includes).to contain_exactly(
{ type: :local,
location: 'myfolder/file1.yml',
- blob: "http://localhost/#{project.full_path}/-/blob/12345/myfolder/file1.yml",
- raw: "http://localhost/#{project.full_path}/-/raw/12345/myfolder/file1.yml",
+ blob: "http://localhost/#{project.full_path}/-/blob/#{sha}/myfolder/file1.yml",
+ raw: "http://localhost/#{project.full_path}/-/raw/#{sha}/myfolder/file1.yml",
extra: {},
context_project: project.full_path,
- context_sha: '12345' },
+ context_sha: sha },
{ type: :local,
- blob: "http://localhost/#{project.full_path}/-/blob/12345/myfolder/file2.yml",
- raw: "http://localhost/#{project.full_path}/-/raw/12345/myfolder/file2.yml",
+ blob: "http://localhost/#{project.full_path}/-/blob/#{sha}/myfolder/file2.yml",
+ raw: "http://localhost/#{project.full_path}/-/raw/#{sha}/myfolder/file2.yml",
location: 'myfolder/file2.yml',
extra: {},
context_project: project.full_path,
- context_sha: '12345' }
+ context_sha: sha }
)
end
end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index c4a6641ff6b..b48a89059bf 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config do
+RSpec.describe Gitlab::Ci::Config, feature_category: :pipeline_authoring do
include StubRequests
let_it_be(:user) { create(:user) }
@@ -305,7 +305,7 @@ RSpec.describe Gitlab::Ci::Config do
it 'raises error' do
expect { config }.to raise_error(
described_class::ConfigError,
- /\!reference \["job-2", "before_script"\] is part of a circular chain/
+ /!reference \["job-2", "before_script"\] is part of a circular chain/
)
end
end
@@ -503,7 +503,7 @@ RSpec.describe Gitlab::Ci::Config do
expect { config }.to raise_error(
described_class::ConfigError,
- 'Resolving config took longer than expected'
+ 'Request timed out when fetching configuration files.'
)
end
end
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index 33474865a93..4b750cf3bcf 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -358,4 +358,22 @@ RSpec.describe Gitlab::Ci::CronParser do
end
end
end
+
+ describe '#match?' do
+ let(:run_date) { Time.zone.local(2021, 3, 2, 1, 0) }
+
+ subject(:matched) { described_class.new(cron, Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE).match?(run_date) }
+
+ context 'when cron matches up' do
+ let(:cron) { '0 1 2 3 *' }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when cron does not match' do
+ let(:cron) { '5 4 3 2 1' }
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/environment_matcher_spec.rb b/spec/lib/gitlab/ci/environment_matcher_spec.rb
new file mode 100644
index 00000000000..172ada1b764
--- /dev/null
+++ b/spec/lib/gitlab/ci/environment_matcher_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::EnvironmentMatcher, feature_category: :continuous_integration do
+ describe '#match?' do
+ context 'when given pattern is a normal string' do
+ subject { described_class.new('production') }
+
+ it 'returns true on an exact match' do
+ expect(subject.match?('production')).to eq true
+ end
+
+ it 'returns false if not an exact match' do
+ expect(subject.match?('productiom')).to eq false
+ end
+ end
+
+ context 'when given pattern has a wildcard' do
+ it 'returns true on wildcard matches', :aggregate_failures do
+ expect(described_class.new('review/*').match?('review/123')).to eq true
+ expect(described_class.new('review/*/*').match?('review/123/456')).to eq true
+ expect(described_class.new('*-this-is-a-pattern-*').match?('abc123-this-is-a-pattern-abc123')).to eq true
+ end
+
+ it 'returns false when not a wildcard match', :aggregate_failures do
+ expect(described_class.new('review/*').match?('review123')).to eq false
+ expect(described_class.new('review/*/*').match?('review/123')).to eq false
+ expect(described_class.new('*-this-is-a-pattern-*').match?('abc123-this-is-a-pattern')).to eq false
+ end
+ end
+
+ context 'when given pattern is nil' do
+ subject { described_class.new(nil) }
+
+ it 'always returns false' do
+ expect(subject.match?('production')).to eq false
+ expect(subject.match?('review/123')).to eq false
+ end
+ end
+
+ context 'when given pattern is an empty string' do
+ subject { described_class.new('') }
+
+ it 'always returns false' do
+ expect(subject.match?('production')).to eq false
+ expect(subject.match?('review/123')).to eq false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb
index cf07e952f26..b836ca395fa 100644
--- a/spec/lib/gitlab/ci/lint_spec.rb
+++ b/spec/lib/gitlab/ci/lint_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Lint do
+RSpec.describe Gitlab::Ci::Lint, feature_category: :pipeline_authoring do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
@@ -337,35 +337,28 @@ RSpec.describe Gitlab::Ci::Lint do
end
end
- context 'pipeline logger' do
- let(:counters) do
- {
- 'count' => a_kind_of(Numeric),
- 'avg' => a_kind_of(Numeric),
- 'sum' => a_kind_of(Numeric),
- 'max' => a_kind_of(Numeric),
- 'min' => a_kind_of(Numeric)
- }
- end
-
- let(:loggable_data) do
+ describe 'pipeline logger' do
+ let(:expected_data) do
{
'class' => 'Gitlab::Ci::Pipeline::Logger',
- 'config_build_context_duration_s' => counters,
- 'config_build_variables_duration_s' => counters,
- 'config_compose_duration_s' => counters,
- 'config_expand_duration_s' => counters,
- 'config_external_process_duration_s' => counters,
- 'config_stages_inject_duration_s' => counters,
- 'config_tags_resolve_duration_s' => counters,
- 'config_yaml_extend_duration_s' => counters,
- 'config_yaml_load_duration_s' => counters,
+ 'config_build_context_duration_s' => a_kind_of(Numeric),
+ 'config_build_variables_duration_s' => a_kind_of(Numeric),
+ 'config_root_duration_s' => a_kind_of(Numeric),
+ 'config_root_compose_duration_s' => a_kind_of(Numeric),
+ 'config_root_compose_jobs_factory_duration_s' => a_kind_of(Numeric),
+ 'config_root_compose_jobs_create_duration_s' => a_kind_of(Numeric),
+ 'config_expand_duration_s' => a_kind_of(Numeric),
+ 'config_external_process_duration_s' => a_kind_of(Numeric),
+ 'config_stages_inject_duration_s' => a_kind_of(Numeric),
+ 'config_tags_resolve_duration_s' => a_kind_of(Numeric),
+ 'config_yaml_extend_duration_s' => a_kind_of(Numeric),
+ 'config_yaml_load_duration_s' => a_kind_of(Numeric),
'pipeline_creation_caller' => 'Gitlab::Ci::Lint',
'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
'pipeline_persisted' => false,
'pipeline_source' => 'unknown',
'project_id' => project&.id,
- 'yaml_process_duration_s' => counters
+ 'yaml_process_duration_s' => a_kind_of(Numeric)
}
end
@@ -403,7 +396,7 @@ RSpec.describe Gitlab::Ci::Lint do
end
it 'creates a log entry' do
- expect(Gitlab::AppJsonLogger).to receive(:info).with(loggable_data)
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(a_hash_including(expected_data))
validate
end
@@ -424,11 +417,11 @@ RSpec.describe Gitlab::Ci::Lint do
let(:project) { nil }
let(:project_nil_loggable_data) do
- loggable_data.except('project_id')
+ expected_data.except('project_id')
end
it 'creates a log entry without project_id' do
- expect(Gitlab::AppJsonLogger).to receive(:info).with(project_nil_loggable_data)
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(a_hash_including(project_nil_loggable_data))
validate
end
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 f09b85aa2c7..dacbe07c8b3 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties do
+RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties, feature_category: :dependency_management do
subject(:parse_source_from_properties) { described_class.parse_source(properties) }
context 'when properties are nil' do
diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb
index 0b094880f69..d06537ac330 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx do
+RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx, feature_category: :dependency_management do
let(:report) { instance_double('Gitlab::Ci::Reports::Sbom::Report') }
let(:report_data) { base_report_data }
let(:raw_report_data) { report_data.to_json }
diff --git a/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb
index e12fa380209..bc97eb2d950 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning do
+RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning, feature_category: :dependency_management do
subject { described_class.source(property_data) }
context 'when all property data is present' do
diff --git a/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb
index f58a463f047..712dc00ec7a 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb
@@ -2,7 +2,8 @@
require "spec_helper"
-RSpec.describe Gitlab::Ci::Parsers::Sbom::Validators::CyclonedxSchemaValidator do
+RSpec.describe Gitlab::Ci::Parsers::Sbom::Validators::CyclonedxSchemaValidator,
+ feature_category: :dependency_management do
# Reports should be valid or invalid according to the specification at
# https://cyclonedx.org/docs/1.4/json/
diff --git a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
index e730afc72b5..c94ed1f8d6d 100644
--- a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
@@ -95,7 +95,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
context 'when all files under schema path are explicitly listed' do
# We only care about the part that comes before report-format.json
# https://rubular.com/r/N8Juz7r8hYDYgD
- filename_regex = /(?<report_type>[-\w]*)\-report-format.json/
+ filename_regex = /(?<report_type>[-\w]*)-report-format.json/
versions = Dir.glob(File.join(schema_path, "*", File::SEPARATOR)).map { |path| path.split("/").last }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb
index 15df5b2f68c..74a68f28f3e 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb
@@ -10,13 +10,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::AssignPartition do
Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user)
end
- let(:pipeline) { build(:ci_pipeline, project: project) }
+ let(:pipeline) { build(:ci_pipeline, project: project, partition_id: nil) }
let(:step) { described_class.new(pipeline, command) }
let(:current_partition_id) { 123 }
describe '#perform!' do
+ include Ci::PartitioningHelpers
+
before do
- allow(Ci::Pipeline).to receive(:current_partition_value) { current_partition_id }
+ stub_current_partition_id(current_partition_id)
end
subject { step.perform! }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb
index 32c92724f62..b2128f77960 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb
@@ -2,11 +2,12 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations do
+RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations, feature_category: :continuous_integration do
let_it_be_with_reload(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user, developer_projects: [project]) }
- let(:pipeline) { Ci::Pipeline.new }
+ # Assigning partition_id here to validate it is being propagated correctly
+ let(:pipeline) { Ci::Pipeline.new(partition_id: ci_testing_partition_id) }
let(:bridge) { nil }
let(:variables_attributes) do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb
index fc3de2a14cd..16deeb6916f 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb
@@ -173,21 +173,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines do
expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running')
end
-
- context 'when feature flag ci_skip_auto_cancelation_on_child_pipelines is disabled' do
- before do
- stub_feature_flags(ci_skip_auto_cancelation_on_child_pipelines: false)
- end
-
- it 'does not cancel the parent pipeline' do
- expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running')
-
- perform
-
- expect(build_statuses(prev_pipeline)).to contain_exactly('success', 'canceled', 'canceled')
- expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running')
- end
- end
end
context 'when the previous pipeline source is webide' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
index 9126c6dab21..68158503628 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
@@ -374,21 +374,57 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do
end
end
+ describe '#observe_creation_duration' do
+ let(:histogram) { instance_double(Prometheus::Client::Histogram) }
+ let(:duration) { 1.hour }
+ let(:command) { described_class.new(project: project) }
+
+ subject(:observe_creation_duration) do
+ command.observe_creation_duration(duration)
+ end
+
+ it 'records the duration as histogram' do
+ expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_creation_duration_histogram)
+ .and_return(histogram)
+ expect(histogram).to receive(:observe)
+ .with({ gitlab: 'false' }, duration.seconds)
+
+ observe_creation_duration
+ end
+
+ context 'when project is gitlab-org/gitlab' do
+ before do
+ allow(project).to receive(:full_path).and_return('gitlab-org/gitlab')
+ end
+
+ it 'tracks the duration with the expected label' do
+ expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_creation_duration_histogram)
+ .and_return(histogram)
+ expect(histogram).to receive(:observe)
+ .with({ gitlab: 'true' }, duration.seconds)
+
+ observe_creation_duration
+ end
+ end
+ end
+
describe '#observe_step_duration' do
+ let(:histogram) { instance_double(Prometheus::Client::Histogram) }
+ let(:duration) { 1.hour }
+ let(:command) { described_class.new }
+
+ subject(:observe_step_duration) do
+ command.observe_step_duration(Gitlab::Ci::Pipeline::Chain::Build, duration)
+ end
+
context 'when ci_pipeline_creation_step_duration_tracking is enabled' do
it 'adds the duration to the step duration histogram' do
- histogram = instance_double(Prometheus::Client::Histogram)
- duration = 1.hour
-
expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_creation_step_duration_histogram)
.and_return(histogram)
expect(histogram).to receive(:observe)
.with({ step: 'Gitlab::Ci::Pipeline::Chain::Build' }, duration.seconds)
- described_class.new.observe_step_duration(
- Gitlab::Ci::Pipeline::Chain::Build,
- duration
- )
+ observe_step_duration
end
end
@@ -398,14 +434,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do
end
it 'does nothing' do
- duration = 1.hour
-
expect(::Gitlab::Ci::Pipeline::Metrics).not_to receive(:pipeline_creation_step_duration_histogram)
- described_class.new.observe_step_duration(
- Gitlab::Ci::Pipeline::Chain::Build,
- duration
- )
+ observe_step_duration
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb
index 7fb5b0b4200..39520149032 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb
@@ -36,9 +36,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments, :aggregate_failu
end
context 'and the pipeline is for a merge request' do
- let(:command) do
- Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user, merge_request: merge_request)
- end
+ let(:pipeline) { build(:ci_pipeline, project: project, stages: [stage], merge_request: merge_request) }
it 'associates the environment with the merge request' do
expect { subject }.to change { Environment.count }.by(1)
@@ -62,9 +60,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments, :aggregate_failu
end
context 'and the pipeline is for a merge request' do
- let(:command) do
- Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user, merge_request: merge_request)
- end
+ let(:pipeline) { build(:ci_pipeline, project: project, stages: [stage], merge_request: merge_request) }
it 'does not associate the environment with the merge request' do
expect { subject }.not_to change { Environment.count }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
index 7aaeee32f49..9373888aada 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
- let_it_be(:project, reload: true) { create(:project, :repository) }
+RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities, feature_category: :pipeline_execution do
+ let(:project) { create(:project, :test_repo) }
let_it_be(:user) { create(:user) }
let(:pipeline) do
diff --git a/spec/lib/gitlab/ci/pipeline/logger_spec.rb b/spec/lib/gitlab/ci/pipeline/logger_spec.rb
index 3af0ebe7484..1c285889d1b 100644
--- a/spec/lib/gitlab/ci/pipeline/logger_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/logger_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
+RSpec.describe ::Gitlab::Ci::Pipeline::Logger, feature_category: :continuous_integration do
let_it_be(:project) { build_stubbed(:project) }
let_it_be(:pipeline) { build_stubbed(:ci_pipeline, project: project) }
@@ -22,61 +22,54 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
end
it 'records durations of instrumented operations' do
- loggable_data = {
+ logger.instrument(:expensive_operation) { 123 }
+
+ expected_data = {
'expensive_operation_duration_s' => {
'count' => 1,
- 'sum' => a_kind_of(Numeric),
- 'avg' => a_kind_of(Numeric),
'max' => a_kind_of(Numeric),
- 'min' => a_kind_of(Numeric)
+ 'sum' => a_kind_of(Numeric)
}
}
-
- logger.instrument(:expensive_operation) { 123 }
- expect(logger.observations_hash).to match(a_hash_including(loggable_data))
+ expect(logger.observations_hash).to match(a_hash_including(expected_data))
end
it 'raises an error when block is not provided' do
expect { logger.instrument(:expensive_operation) }
.to raise_error(ArgumentError, 'block not given')
end
+
+ context 'when once: true' do
+ it 'logs only one observation' do
+ logger.instrument(:expensive_operation, once: true) { 123 }
+ logger.instrument(:expensive_operation, once: true) { 123 }
+
+ expected_data = {
+ 'expensive_operation_duration_s' => a_kind_of(Numeric)
+ }
+ expect(logger.observations_hash).to match(a_hash_including(expected_data))
+ end
+ end
end
- describe '#instrument_with_sql', :request_store do
- subject(:instrument_with_sql) do
- logger.instrument_with_sql(:expensive_operation, &operation)
+ describe '#instrument_once_with_sql', :request_store do
+ subject(:instrument_once_with_sql) do
+ logger.instrument_once_with_sql(:expensive_operation, &operation)
end
- def loggable_data(count:, db_count: nil)
+ def expected_data(count:, db_count: nil)
database_name = Ci::ApplicationRecord.connection.pool.db_config.name
- keys = %W[
- expensive_operation_duration_s
- expensive_operation_db_count
- expensive_operation_db_primary_count
- expensive_operation_db_primary_duration_s
- expensive_operation_db_#{database_name}_count
- expensive_operation_db_#{database_name}_duration_s
- ]
-
- data = keys.each.with_object({}) do |key, accumulator|
- accumulator[key] = {
- 'count' => count,
- 'avg' => a_kind_of(Numeric),
- 'sum' => a_kind_of(Numeric),
- 'max' => a_kind_of(Numeric),
- 'min' => a_kind_of(Numeric)
- }
- end
-
- if db_count
- data['expensive_operation_db_count']['max'] = db_count
- data['expensive_operation_db_count']['min'] = db_count
- data['expensive_operation_db_count']['avg'] = db_count
- data['expensive_operation_db_count']['sum'] = count * db_count
- end
+ total_db_count = count * db_count if db_count
- data
+ {
+ "expensive_operation_duration_s" => a_kind_of(Numeric),
+ "expensive_operation_db_count" => total_db_count || a_kind_of(Numeric),
+ "expensive_operation_db_primary_count" => a_kind_of(Numeric),
+ "expensive_operation_db_primary_duration_s" => a_kind_of(Numeric),
+ "expensive_operation_db_#{database_name}_count" => a_kind_of(Numeric),
+ "expensive_operation_db_#{database_name}_duration_s" => a_kind_of(Numeric)
+ }
end
context 'with a single query' do
@@ -85,10 +78,10 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
it { is_expected.to eq(operation.call) }
it 'includes SQL metrics' do
- instrument_with_sql
+ instrument_once_with_sql
expect(logger.observations_hash)
- .to match(a_hash_including(loggable_data(count: 1, db_count: 1)))
+ .to match(a_hash_including(expected_data(count: 1, db_count: 1)))
end
end
@@ -98,21 +91,10 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
it { is_expected.to eq(operation.call) }
it 'includes SQL metrics' do
- instrument_with_sql
-
- expect(logger.observations_hash)
- .to match(a_hash_including(loggable_data(count: 1, db_count: 2)))
- end
- end
-
- context 'with multiple observations' do
- let(:operation) { -> { Ci::Build.count + Ci::Bridge.count } }
-
- it 'includes SQL metrics' do
- 2.times { logger.instrument_with_sql(:expensive_operation, &operation) }
+ instrument_once_with_sql
expect(logger.observations_hash)
- .to match(a_hash_including(loggable_data(count: 2, db_count: 2)))
+ .to match(a_hash_including(expected_data(count: 1, db_count: 2)))
end
end
@@ -122,7 +104,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
it { is_expected.to eq(operation.call) }
it 'does not include SQL metrics' do
- instrument_with_sql
+ instrument_once_with_sql
expect(logger.observations_hash.keys)
.to match_array(['expensive_operation_duration_s'])
@@ -132,14 +114,40 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
describe '#observe' do
it 'records durations of observed operations' do
- loggable_data = {
+ expect(logger.observe(:pipeline_creation_duration_s, 30)).to be_truthy
+
+ expected_data = {
'pipeline_creation_duration_s' => {
- 'avg' => 30, 'sum' => 30, 'count' => 1, 'max' => 30, 'min' => 30
+ 'sum' => 30, 'count' => 1, 'max' => 30
}
}
+ expect(logger.observations_hash).to match(a_hash_including(expected_data))
+ end
- expect(logger.observe(:pipeline_creation_duration_s, 30)).to be_truthy
- expect(logger.observations_hash).to match(a_hash_including(loggable_data))
+ context 'when once: true' do
+ it 'records the latest observation' do
+ expect(logger.observe(:pipeline_creation_duration_s, 20, once: true)).to be_truthy
+ expect(logger.observe(:pipeline_creation_duration_s, 30, once: true)).to be_truthy
+
+ expected_data = {
+ 'pipeline_creation_duration_s' => 30
+ }
+ expect(logger.observations_hash).to match(a_hash_including(expected_data))
+ end
+
+ it 'logs data as expected' do
+ expect(logger.observe(:pipeline_creation_duration_s, 30, once: true)).to be_truthy
+ expect(logger.observe(:pipeline_operation_x_duration_s, 20)).to be_truthy
+ expect(logger.observe(:pipeline_operation_x_duration_s, 20)).to be_truthy
+
+ expected_data = {
+ 'pipeline_creation_duration_s' => 30,
+ 'pipeline_operation_x_duration_s' => {
+ 'sum' => 40, 'count' => 2, 'max' => 20
+ }
+ }
+ expect(logger.observations_hash).to match(a_hash_including(expected_data))
+ end
end
end
@@ -158,8 +166,11 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
context 'when the feature flag is enabled' do
let(:flag) { true }
- let(:loggable_data) do
+ let(:expected_data) do
{
+ 'correlation_id' => a_kind_of(String),
+ 'meta.project' => project.full_path,
+ 'meta.root_namespace' => project.root_namespace.full_path,
'class' => described_class.name.to_s,
'pipeline_id' => pipeline.id,
'pipeline_persisted' => true,
@@ -168,10 +179,10 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
'pipeline_creation_caller' => 'source',
'pipeline_source' => pipeline.source,
'pipeline_save_duration_s' => {
- 'avg' => 60, 'sum' => 60, 'count' => 1, 'max' => 60, 'min' => 60
+ 'sum' => 60, 'count' => 1, 'max' => 60
},
'pipeline_creation_duration_s' => {
- 'avg' => 20, 'sum' => 40, 'count' => 2, 'max' => 30, 'min' => 10
+ 'sum' => 40, 'count' => 2, 'max' => 30
}
}
end
@@ -179,7 +190,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
it 'logs to application.json' do
expect(Gitlab::AppJsonLogger)
.to receive(:info)
- .with(a_hash_including(loggable_data))
+ .with(a_hash_including(expected_data))
.and_call_original
expect(commit).to be_truthy
@@ -200,28 +211,43 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
expect(Gitlab::AppJsonLogger)
.to receive(:info)
- .with(a_hash_including(loggable_data))
+ .with(a_hash_including(expected_data))
.and_call_original
expect(commit).to be_truthy
end
+
+ context 'with unexistent observations in condition' do
+ it 'does not commit the log' do
+ logger.log_when do |observations|
+ value = observations['non_existent_value']
+ next false unless value
+
+ value > 0
+ end
+
+ expect(Gitlab::AppJsonLogger).not_to receive(:info)
+
+ expect(commit).to be_falsey
+ end
+ end
end
context 'when project is not passed and pipeline is not persisted' do
let(:project) {}
let(:pipeline) { build(:ci_pipeline) }
- let(:loggable_data) do
+ let(:expected_data) do
{
'class' => described_class.name.to_s,
'pipeline_persisted' => false,
'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
'pipeline_creation_caller' => 'source',
'pipeline_save_duration_s' => {
- 'avg' => 60, 'sum' => 60, 'count' => 1, 'max' => 60, 'min' => 60
+ 'sum' => 60, 'count' => 1, 'max' => 60
},
'pipeline_creation_duration_s' => {
- 'avg' => 20, 'sum' => 40, 'count' => 2, 'max' => 30, 'min' => 10
+ 'sum' => 40, 'count' => 2, 'max' => 30
}
}
end
@@ -229,7 +255,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
it 'logs to application.json' do
expect(Gitlab::AppJsonLogger)
.to receive(:info)
- .with(a_hash_including(loggable_data))
+ .with(a_hash_including(expected_data))
.and_call_original
expect(commit).to be_truthy
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
index 910c12389c3..fb8020bf43e 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
@@ -6,8 +6,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:head_sha) { project.repository.head_commit.id }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: head_sha) }
+ let(:index) { 1 }
- let(:processor) { described_class.new(pipeline, config) }
+ let(:processor) { described_class.new(pipeline, config, index) }
describe '#attributes' do
subject { processor.attributes }
@@ -40,10 +41,12 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do
{ key: { files: files } }
end
- it 'uses default key' do
- expected = { key: 'default' }
+ context 'without a prefix' do
+ it 'uses default key with an index as a prefix' do
+ expected = { key: '1-default' }
- is_expected.to include(expected)
+ is_expected.to include(expected)
+ end
end
end
@@ -57,13 +60,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do
}
end
- it 'builds a string key' do
- expected = {
- key: '703ecc8fef1635427a1f86a8a1a308831c122392',
- paths: ['vendor/ruby']
- }
+ context 'without a prefix' do
+ it 'builds a string key with an index as a prefix' do
+ expected = {
+ key: '1-703ecc8fef1635427a1f86a8a1a308831c122392',
+ paths: ['vendor/ruby']
+ }
- is_expected.to include(expected)
+ is_expected.to include(expected)
+ end
end
end
@@ -107,10 +112,12 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do
}
end
- it 'builds a string key' do
- expected = { key: '74bf43fb1090f161bdd4e265802775dbda2f03d1' }
+ context 'without a prefix' do
+ it 'builds a string key with an index as a prefix' do
+ expected = { key: '1-74bf43fb1090f161bdd4e265802775dbda2f03d1' }
- is_expected.to include(expected)
+ is_expected.to include(expected)
+ end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 75f6a773c2d..1f7f800e238 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
+RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_authoring do
let_it_be_with_reload(:project) { create(:project, :repository) }
let_it_be(:head_sha) { project.repository.head_commit.id }
@@ -11,861 +11,954 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
let(:seed_context) { Gitlab::Ci::Pipeline::Seed::Context.new(pipeline, root_variables: root_variables) }
let(:attributes) { { name: 'rspec', ref: 'master', scheduling_type: :stage, when: 'on_success' } }
let(:previous_stages) { [] }
- let(:current_stage) { double(seeds_names: [attributes[:name]]) }
+ let(:current_stage) { instance_double(Gitlab::Ci::Pipeline::Seed::Stage, seeds_names: [attributes[:name]]) }
+ let(:current_ci_stage) { build(:ci_stage, pipeline: pipeline) }
- let(:seed_build) { described_class.new(seed_context, attributes, previous_stages + [current_stage]) }
+ let(:seed_build) { described_class.new(seed_context, attributes, previous_stages + [current_stage], current_ci_stage) }
- describe '#attributes' do
- subject { seed_build.attributes }
+ shared_examples 'build seed' do
+ describe '#attributes' do
+ subject { seed_build.attributes }
- it { is_expected.to be_a(Hash) }
- it { is_expected.to include(:name, :project, :ref) }
+ it { is_expected.to be_a(Hash) }
+ it { is_expected.to include(:name, :project, :ref) }
- context 'with job:when' do
- let(:attributes) { { name: 'rspec', ref: 'master', when: 'on_failure' } }
+ context 'with job:when' do
+ let(:attributes) { { name: 'rspec', ref: 'master', when: 'on_failure' } }
- it { is_expected.to include(when: 'on_failure') }
- end
-
- context 'with job:when:delayed' do
- let(:attributes) { { name: 'rspec', ref: 'master', when: 'delayed', start_in: '3 hours' } }
-
- it { is_expected.to include(when: 'delayed', start_in: '3 hours') }
- end
-
- context 'with job:rules:[when:]' do
- context 'is matched' do
- let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'always' }] } }
-
- it { is_expected.to include(when: 'always') }
+ it { is_expected.to include(when: 'on_failure') }
end
- context 'is not matched' do
- let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'always' }] } }
-
- it { is_expected.to include(when: 'never') }
- end
- end
-
- context 'with job:rules:[when:delayed]' do
- context 'is matched' do
- let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] } }
+ context 'with job:when:delayed' do
+ let(:attributes) { { name: 'rspec', ref: 'master', when: 'delayed', options: { start_in: '3 hours' } } }
it { is_expected.to include(when: 'delayed', options: { start_in: '3 hours' }) }
end
- context 'is not matched' do
- let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'delayed', start_in: '3 hours' }] } }
-
- it { is_expected.to include(when: 'never') }
- end
- end
-
- context 'with job: rules but no explicit when:' do
- let(:base_attributes) { { name: 'rspec', ref: 'master' } }
-
- context 'with a manual job' do
- context 'with a matched rule' do
- let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR == null' }]) }
+ context 'with job:rules:[when:]' do
+ context 'is matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'always' }] } }
- it { is_expected.to include(when: 'manual') }
+ it { is_expected.to include(when: 'always') }
end
context 'is not matched' do
- let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR != null' }]) }
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'always' }] } }
it { is_expected.to include(when: 'never') }
end
end
- context 'with an automatic job' do
+ context 'with job:rules:[when:delayed]' do
context 'is matched' do
- let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR == null' }]) }
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] } }
- it { is_expected.to include(when: 'on_success') }
+ it { is_expected.to include(when: 'delayed', options: { start_in: '3 hours' }) }
end
context 'is not matched' do
- let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR != null' }]) }
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'delayed', start_in: '3 hours' }] } }
it { is_expected.to include(when: 'never') }
end
end
- end
- context 'with job:rules:[variables:]' do
- let(:attributes) do
- { name: 'rspec',
- ref: 'master',
- job_variables: [{ key: 'VAR1', value: 'var 1' },
- { key: 'VAR2', value: 'var 2' }],
- rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] }
- end
+ context 'with job: rules but no explicit when:' do
+ let(:base_attributes) { { name: 'rspec', ref: 'master' } }
- it do
- is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1' },
- { key: 'VAR3', value: 'var 3' },
- { key: 'VAR2', value: 'var 2' }])
- end
- end
+ context 'with a manual job' do
+ context 'with a matched rule' do
+ let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR == null' }]) }
- context 'with job:tags' do
- let(:attributes) do
- {
- name: 'rspec',
- ref: 'master',
- job_variables: [{ key: 'VARIABLE', value: 'value' }],
- tag_list: ['static-tag', '$VARIABLE', '$NO_VARIABLE']
- }
- end
+ it { is_expected.to include(when: 'manual') }
+ end
- it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) }
- it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) }
- end
+ context 'is not matched' do
+ let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR != null' }]) }
- context 'with cache:key' do
- let(:attributes) do
- {
- name: 'rspec',
- ref: 'master',
- cache: [{
- key: 'a-value'
- }]
- }
- end
+ it { is_expected.to include(when: 'never') }
+ end
+ end
+
+ context 'with an automatic job' do
+ context 'is matched' do
+ let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR == null' }]) }
+
+ it { is_expected.to include(when: 'on_success') }
+ end
- it { is_expected.to include(options: { cache: [a_hash_including(key: 'a-value')] }) }
+ context 'is not matched' do
+ let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR != null' }]) }
+
+ it { is_expected.to include(when: 'never') }
+ end
+ end
+ end
- context 'with cache:key:files' do
+ context 'with job:rules:[variables:]' do
let(:attributes) do
- {
- name: 'rspec',
+ { name: 'rspec',
ref: 'master',
- cache: [{
- key: {
- files: ['VERSION']
- }
- }]
- }
+ job_variables: [{ key: 'VAR1', value: 'var 1' },
+ { key: 'VAR2', value: 'var 2' }],
+ rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] }
end
- it 'includes cache options' do
- cache_options = {
- options: {
- cache: [a_hash_including(key: 'f155568ad0933d8358f66b846133614f76dd0ca4')]
- }
- }
+ it do
+ is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1' },
+ { key: 'VAR3', value: 'var 3' },
+ { key: 'VAR2', value: 'var 2' }])
+ end
- is_expected.to include(cache_options)
+ it 'expects the same results on to_resource' do
+ expect(seed_build.to_resource.yaml_variables).to include({ key: 'VAR1', value: 'new var 1' },
+ { key: 'VAR3', value: 'var 3' },
+ { key: 'VAR2', value: 'var 2' })
end
end
- context 'with cache:key:prefix' do
+ context 'with job:tags' do
let(:attributes) do
{
name: 'rspec',
ref: 'master',
- cache: [{
- key: {
- prefix: 'something'
- }
- }]
+ job_variables: [{ key: 'VARIABLE', value: 'value' }],
+ tag_list: ['static-tag', '$VARIABLE', '$NO_VARIABLE']
}
end
- it { is_expected.to include(options: { cache: [a_hash_including( key: 'something-default' )] }) }
+ it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) }
+ it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) }
end
- context 'with cache:key:files and prefix' do
+ context 'with cache:key' do
let(:attributes) do
{
name: 'rspec',
ref: 'master',
cache: [{
- key: {
- files: ['VERSION'],
- prefix: 'something'
- }
+ key: 'a-value'
}]
}
end
- it 'includes cache options' do
- cache_options = {
- options: {
- cache: [a_hash_including(key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4')]
+ it { is_expected.to include(options: { cache: [a_hash_including(key: 'a-value')] }) }
+
+ context 'with cache:key:files' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ cache: [{
+ key: {
+ files: ['VERSION']
+ }
+ }]
}
- }
+ end
- is_expected.to include(cache_options)
- end
- end
- end
+ it 'includes cache options' do
+ cache_options = {
+ options: {
+ cache: [a_hash_including(key: '0-f155568ad0933d8358f66b846133614f76dd0ca4')]
+ }
+ }
- context 'with empty cache' do
- let(:attributes) do
- {
- name: 'rspec',
- ref: 'master',
- cache: {}
- }
- end
+ is_expected.to include(cache_options)
+ end
+ end
- it { is_expected.to include({}) }
- end
+ context 'with cache:key:prefix' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ cache: [{
+ key: {
+ prefix: 'something'
+ }
+ }]
+ }
+ end
- context 'with allow_failure' do
- let(:options) do
- { allow_failure_criteria: { exit_codes: [42] } }
- end
+ it { is_expected.to include(options: { cache: [a_hash_including( key: 'something-default' )] }) }
+ end
- let(:rules) do
- [{ if: '$VAR == null', when: 'always' }]
- end
+ context 'with cache:key:files and prefix' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ cache: [{
+ key: {
+ files: ['VERSION'],
+ prefix: 'something'
+ }
+ }]
+ }
+ end
- let(:attributes) do
- {
- name: 'rspec',
- ref: 'master',
- options: options,
- rules: rules
- }
- end
+ it 'includes cache options' do
+ cache_options = {
+ options: {
+ cache: [a_hash_including(key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4')]
+ }
+ }
- context 'when rules does not override allow_failure' do
- it { is_expected.to match a_hash_including(options: options) }
+ is_expected.to include(cache_options)
+ end
+ end
end
- context 'when rules set allow_failure to true' do
- let(:rules) do
- [{ if: '$VAR == null', when: 'always', allow_failure: true }]
+ context 'with empty cache' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ cache: {}
+ }
end
- it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) }
+ it { is_expected.to include({}) }
end
- context 'when rules set allow_failure to false' do
- let(:rules) do
- [{ if: '$VAR == null', when: 'always', allow_failure: false }]
+ context 'with allow_failure' do
+ let(:options) do
+ { allow_failure_criteria: { exit_codes: [42] } }
end
- it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) }
- end
- end
-
- context 'with workflow:rules:[variables:]' do
- let(:attributes) do
- { name: 'rspec',
- ref: 'master',
- yaml_variables: [{ key: 'VAR2', value: 'var 2' },
- { key: 'VAR3', value: 'var 3' }],
- job_variables: [{ key: 'VAR2', value: 'var 2' },
- { key: 'VAR3', value: 'var 3' }],
- root_variables_inheritance: root_variables_inheritance }
- end
+ let(:rules) do
+ [{ if: '$VAR == null', when: 'always' }]
+ end
- context 'when the pipeline has variables' do
- let(:root_variables) do
- [{ key: 'VAR1', value: 'var overridden pipeline 1' },
- { key: 'VAR2', value: 'var pipeline 2' },
- { key: 'VAR3', value: 'var pipeline 3' },
- { key: 'VAR4', value: 'new var pipeline 4' }]
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ options: options,
+ rules: rules
+ }
end
- context 'when root_variables_inheritance is true' do
- let(:root_variables_inheritance) { true }
+ context 'when rules does not override allow_failure' do
+ it { is_expected.to match a_hash_including(options: options) }
+ end
- it 'returns calculated yaml variables' do
- expect(subject[:yaml_variables]).to match_array(
- [{ key: 'VAR1', value: 'var overridden pipeline 1' },
- { key: 'VAR2', value: 'var 2' },
- { key: 'VAR3', value: 'var 3' },
- { key: 'VAR4', value: 'new var pipeline 4' }]
- )
+ context 'when rules set allow_failure to true' do
+ let(:rules) do
+ [{ if: '$VAR == null', when: 'always', allow_failure: true }]
end
- end
- context 'when root_variables_inheritance is false' do
- let(:root_variables_inheritance) { false }
+ it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) }
- it 'returns job variables' do
- expect(subject[:yaml_variables]).to match_array(
- [{ key: 'VAR2', value: 'var 2' },
- { key: 'VAR3', value: 'var 3' }]
- )
- end
- end
+ context 'when options contain other static values' do
+ let(:options) do
+ { image: 'busybox', allow_failure_criteria: { exit_codes: [42] } }
+ end
- context 'when root_variables_inheritance is an array' do
- let(:root_variables_inheritance) { %w(VAR1 VAR2 VAR3) }
+ it { is_expected.to match a_hash_including(options: { image: 'busybox', allow_failure_criteria: nil }) }
- it 'returns calculated yaml variables' do
- expect(subject[:yaml_variables]).to match_array(
- [{ key: 'VAR1', value: 'var overridden pipeline 1' },
- { key: 'VAR2', value: 'var 2' },
- { key: 'VAR3', value: 'var 3' }]
- )
+ it 'deep merges options when exporting to_resource' do
+ expect(seed_build.to_resource.options).to match a_hash_including(
+ image: 'busybox', allow_failure_criteria: nil
+ )
+ end
end
end
- end
- context 'when the pipeline has not a variable' do
- let(:root_variables_inheritance) { true }
+ context 'when rules set allow_failure to false' do
+ let(:rules) do
+ [{ if: '$VAR == null', when: 'always', allow_failure: false }]
+ end
- it 'returns seed yaml variables' do
- expect(subject[:yaml_variables]).to match_array(
- [{ key: 'VAR2', value: 'var 2' },
- { key: 'VAR3', value: 'var 3' }])
+ it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) }
end
end
- end
- context 'when the job rule depends on variables' do
- let(:attributes) do
- { name: 'rspec',
- ref: 'master',
- yaml_variables: [{ key: 'VAR1', value: 'var 1' }],
- job_variables: [{ key: 'VAR1', value: 'var 1' }],
- root_variables_inheritance: root_variables_inheritance,
- rules: rules }
- end
+ context 'with workflow:rules:[variables:]' do
+ let(:attributes) do
+ { name: 'rspec',
+ ref: 'master',
+ yaml_variables: [{ key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' }],
+ job_variables: [{ key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' }],
+ root_variables_inheritance: root_variables_inheritance }
+ end
+
+ context 'when the pipeline has variables' do
+ let(:root_variables) do
+ [{ key: 'VAR1', value: 'var overridden pipeline 1' },
+ { key: 'VAR2', value: 'var pipeline 2' },
+ { key: 'VAR3', value: 'var pipeline 3' },
+ { key: 'VAR4', value: 'new var pipeline 4' }]
+ end
- let(:root_variables_inheritance) { true }
+ context 'when root_variables_inheritance is true' do
+ let(:root_variables_inheritance) { true }
- context 'when the rules use job variables' do
- let(:rules) do
- [{ if: '$VAR1 == "var 1"', variables: { VAR1: 'overridden var 1', VAR2: 'new var 2' } }]
+ it 'returns calculated yaml variables' do
+ expect(subject[:yaml_variables]).to match_array(
+ [{ key: 'VAR1', value: 'var overridden pipeline 1' },
+ { key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' },
+ { key: 'VAR4', value: 'new var pipeline 4' }]
+ )
+ end
+ end
+
+ context 'when root_variables_inheritance is false' do
+ let(:root_variables_inheritance) { false }
+
+ it 'returns job variables' do
+ expect(subject[:yaml_variables]).to match_array(
+ [{ key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' }]
+ )
+ end
+ end
+
+ context 'when root_variables_inheritance is an array' do
+ let(:root_variables_inheritance) { %w(VAR1 VAR2 VAR3) }
+
+ it 'returns calculated yaml variables' do
+ expect(subject[:yaml_variables]).to match_array(
+ [{ key: 'VAR1', value: 'var overridden pipeline 1' },
+ { key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' }]
+ )
+ end
+ end
end
- it 'recalculates the variables' do
- expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' },
- { key: 'VAR2', value: 'new var 2' })
+ context 'when the pipeline has not a variable' do
+ let(:root_variables_inheritance) { true }
+
+ it 'returns seed yaml variables' do
+ expect(subject[:yaml_variables]).to match_array(
+ [{ key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' }])
+ end
end
end
- context 'when the rules use root variables' do
- let(:root_variables) do
- [{ key: 'VAR2', value: 'var pipeline 2' }]
+ context 'when the job rule depends on variables' do
+ let(:attributes) do
+ { name: 'rspec',
+ ref: 'master',
+ yaml_variables: [{ key: 'VAR1', value: 'var 1' }],
+ job_variables: [{ key: 'VAR1', value: 'var 1' }],
+ root_variables_inheritance: root_variables_inheritance,
+ rules: rules }
end
- let(:rules) do
- [{ if: '$VAR2 == "var pipeline 2"', variables: { VAR1: 'overridden var 1', VAR2: 'overridden var 2' } }]
- end
+ let(:root_variables_inheritance) { true }
+
+ context 'when the rules use job variables' do
+ let(:rules) do
+ [{ if: '$VAR1 == "var 1"', variables: { VAR1: 'overridden var 1', VAR2: 'new var 2' } }]
+ end
- it 'recalculates the variables' do
- expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' },
- { key: 'VAR2', value: 'overridden var 2' })
+ it 'recalculates the variables' do
+ expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' },
+ { key: 'VAR2', value: 'new var 2' })
+ end
end
- context 'when the root_variables_inheritance is false' do
- let(:root_variables_inheritance) { false }
+ context 'when the rules use root variables' do
+ let(:root_variables) do
+ [{ key: 'VAR2', value: 'var pipeline 2' }]
+ end
- it 'does not recalculate the variables' do
- expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' })
+ let(:rules) do
+ [{ if: '$VAR2 == "var pipeline 2"', variables: { VAR1: 'overridden var 1', VAR2: 'overridden var 2' } }]
+ end
+
+ it 'recalculates the variables' do
+ expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' },
+ { key: 'VAR2', value: 'overridden var 2' })
end
- end
- end
- end
- end
- describe '#bridge?' do
- subject { seed_build.bridge? }
+ context 'when the root_variables_inheritance is false' do
+ let(:root_variables_inheritance) { false }
- context 'when job is a downstream bridge' do
- let(:attributes) do
- { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } }
+ it 'does not recalculate the variables' do
+ expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' })
+ end
+ end
+ end
end
+ end
- it { is_expected.to be_truthy }
+ describe '#bridge?' do
+ subject { seed_build.bridge? }
- context 'when trigger definition is empty' do
+ context 'when job is a downstream bridge' do
let(:attributes) do
- { name: 'rspec', ref: 'master', options: { trigger: '' } }
+ { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } }
end
- it { is_expected.to be_falsey }
- end
- end
+ it { is_expected.to be_truthy }
- context 'when job is an upstream bridge' do
- let(:attributes) do
- { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: 'my/project' } } }
- end
+ context 'when trigger definition is empty' do
+ let(:attributes) do
+ { name: 'rspec', ref: 'master', options: { trigger: '' } }
+ end
- it { is_expected.to be_truthy }
+ it { is_expected.to be_falsey }
+ end
+ end
- context 'when upstream definition is empty' do
+ context 'when job is an upstream bridge' do
let(:attributes) do
- { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: '' } } }
+ { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: 'my/project' } } }
end
- it { is_expected.to be_falsey }
- end
- end
+ it { is_expected.to be_truthy }
- context 'when job is not a bridge' do
- it { is_expected.to be_falsey }
- end
- end
+ context 'when upstream definition is empty' do
+ let(:attributes) do
+ { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: '' } } }
+ end
- describe '#to_resource' do
- subject { seed_build.to_resource }
+ it { is_expected.to be_falsey }
+ end
+ end
- it 'memoizes a resource object' do
- expect(subject.object_id).to eq seed_build.to_resource.object_id
+ context 'when job is not a bridge' do
+ it { is_expected.to be_falsey }
+ end
end
- it 'can not be persisted without explicit assignment' do
- pipeline.save!
+ describe '#to_resource' do
+ subject { seed_build.to_resource }
- expect(subject).not_to be_persisted
- end
- end
+ it 'memoizes a resource object' do
+ expect(subject.object_id).to eq seed_build.to_resource.object_id
+ end
- describe 'applying job inclusion policies' do
- subject { seed_build }
+ it 'can not be persisted without explicit assignment' do
+ pipeline.save!
- context 'when no branch policy is specified' do
- let(:attributes) do
- { name: 'rspec' }
+ expect(subject).not_to be_persisted
end
-
- it { is_expected.to be_included }
end
- context 'when branch policy does not match' do
- context 'when using only' do
- let(:attributes) do
- { name: 'rspec', only: { refs: ['deploy'] } }
- end
-
- it { is_expected.not_to be_included }
- end
+ describe 'applying job inclusion policies' do
+ subject { seed_build }
- context 'when using except' do
+ context 'when no branch policy is specified' do
let(:attributes) do
- { name: 'rspec', except: { refs: ['deploy'] } }
+ { name: 'rspec' }
end
it { is_expected.to be_included }
end
- context 'with both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: %w[deploy] },
- except: { refs: %w[deploy] }
- }
+ context 'when branch policy does not match' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: ['deploy'] } }
+ end
+
+ it { is_expected.not_to be_included }
end
- it { is_expected.not_to be_included }
- end
- end
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: ['deploy'] } }
+ end
- context 'when branch regexp policy does not match' do
- context 'when using only' do
- let(:attributes) do
- { name: 'rspec', only: { refs: %w[/^deploy$/] } }
+ it { is_expected.to be_included }
end
- it { is_expected.not_to be_included }
- end
+ context 'with both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: %w[deploy] },
+ except: { refs: %w[deploy] }
+ }
+ end
- context 'when using except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: %w[/^deploy$/] } }
+ it { is_expected.not_to be_included }
end
-
- it { is_expected.to be_included }
end
- context 'with both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: %w[/^deploy$/] },
- except: { refs: %w[/^deploy$/] }
- }
+ context 'when branch regexp policy does not match' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: %w[/^deploy$/] } }
+ end
+
+ it { is_expected.not_to be_included }
end
- it { is_expected.not_to be_included }
- end
- end
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: %w[/^deploy$/] } }
+ end
- context 'when branch policy matches' do
- context 'when using only' do
- let(:attributes) do
- { name: 'rspec', only: { refs: %w[deploy master] } }
+ it { is_expected.to be_included }
end
- it { is_expected.to be_included }
- end
+ context 'with both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: %w[/^deploy$/] },
+ except: { refs: %w[/^deploy$/] }
+ }
+ end
- context 'when using except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: %w[deploy master] } }
+ it { is_expected.not_to be_included }
end
-
- it { is_expected.not_to be_included }
end
- context 'when using both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: %w[deploy master] },
- except: { refs: %w[deploy master] }
- }
+ context 'when branch policy matches' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: %w[deploy master] } }
+ end
+
+ it { is_expected.to be_included }
end
- it { is_expected.not_to be_included }
- end
- end
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: %w[deploy master] } }
+ end
- context 'when keyword policy matches' do
- context 'when using only' do
- let(:attributes) do
- { name: 'rspec', only: { refs: %w[branches] } }
+ it { is_expected.not_to be_included }
end
- it { is_expected.to be_included }
- end
+ context 'when using both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: %w[deploy master] },
+ except: { refs: %w[deploy master] }
+ }
+ end
- context 'when using except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: %w[branches] } }
+ it { is_expected.not_to be_included }
end
-
- it { is_expected.not_to be_included }
end
- context 'when using both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: %w[branches] },
- except: { refs: %w[branches] }
- }
+ context 'when keyword policy matches' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: %w[branches] } }
+ end
+
+ it { is_expected.to be_included }
end
- it { is_expected.not_to be_included }
- end
- end
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: %w[branches] } }
+ end
- context 'when keyword policy does not match' do
- context 'when using only' do
- let(:attributes) do
- { name: 'rspec', only: { refs: %w[tags] } }
+ it { is_expected.not_to be_included }
end
- it { is_expected.not_to be_included }
- end
+ context 'when using both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: %w[branches] },
+ except: { refs: %w[branches] }
+ }
+ end
- context 'when using except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: %w[tags] } }
+ it { is_expected.not_to be_included }
end
-
- it { is_expected.to be_included }
end
- context 'when using both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: %w[tags] },
- except: { refs: %w[tags] }
- }
+ context 'when keyword policy does not match' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: %w[tags] } }
+ end
+
+ it { is_expected.not_to be_included }
end
- it { is_expected.not_to be_included }
- end
- end
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: %w[tags] } }
+ end
- context 'with source-keyword policy' do
- using RSpec::Parameterized
+ it { is_expected.to be_included }
+ end
- let(:pipeline) do
- build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source, project: project)
- end
+ context 'when using both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: %w[tags] },
+ except: { refs: %w[tags] }
+ }
+ end
- context 'matches' do
- where(:keyword, :source) do
- [
- %w[pushes push],
- %w[web web],
- %w[triggers trigger],
- %w[schedules schedule],
- %w[api api],
- %w[external external]
- ]
+ it { is_expected.not_to be_included }
end
+ end
- with_them do
- context 'using an only policy' do
- let(:attributes) do
- { name: 'rspec', only: { refs: [keyword] } }
- end
+ context 'with source-keyword policy' do
+ using RSpec::Parameterized
- it { is_expected.to be_included }
+ let(:pipeline) do
+ build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source, project: project)
+ end
+
+ context 'matches' do
+ where(:keyword, :source) do
+ [
+ %w[pushes push],
+ %w[web web],
+ %w[triggers trigger],
+ %w[schedules schedule],
+ %w[api api],
+ %w[external external]
+ ]
end
- context 'using an except policy' do
- let(:attributes) do
- { name: 'rspec', except: { refs: [keyword] } }
+ with_them do
+ context 'using an only policy' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: [keyword] } }
+ end
+
+ it { is_expected.to be_included }
end
- it { is_expected.not_to be_included }
- end
+ context 'using an except policy' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: [keyword] } }
+ end
- context 'using both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: [keyword] },
- except: { refs: [keyword] }
- }
+ it { is_expected.not_to be_included }
end
- it { is_expected.not_to be_included }
+ context 'using both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: [keyword] },
+ except: { refs: [keyword] }
+ }
+ end
+
+ it { is_expected.not_to be_included }
+ end
end
end
- end
- context 'non-matches' do
- where(:keyword, :source) do
- %w[web trigger schedule api external].map { |source| ['pushes', source] } +
- %w[push trigger schedule api external].map { |source| ['web', source] } +
- %w[push web schedule api external].map { |source| ['triggers', source] } +
- %w[push web trigger api external].map { |source| ['schedules', source] } +
- %w[push web trigger schedule external].map { |source| ['api', source] } +
- %w[push web trigger schedule api].map { |source| ['external', source] }
- end
+ context 'non-matches' do
+ where(:keyword, :source) do
+ %w[web trigger schedule api external].map { |source| ['pushes', source] } +
+ %w[push trigger schedule api external].map { |source| ['web', source] } +
+ %w[push web schedule api external].map { |source| ['triggers', source] } +
+ %w[push web trigger api external].map { |source| ['schedules', source] } +
+ %w[push web trigger schedule external].map { |source| ['api', source] } +
+ %w[push web trigger schedule api].map { |source| ['external', source] }
+ end
- with_them do
- context 'using an only policy' do
- let(:attributes) do
- { name: 'rspec', only: { refs: [keyword] } }
+ with_them do
+ context 'using an only policy' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: [keyword] } }
+ end
+
+ it { is_expected.not_to be_included }
end
- it { is_expected.not_to be_included }
- end
+ context 'using an except policy' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: [keyword] } }
+ end
- context 'using an except policy' do
- let(:attributes) do
- { name: 'rspec', except: { refs: [keyword] } }
+ it { is_expected.to be_included }
end
- it { is_expected.to be_included }
- end
+ context 'using both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: [keyword] },
+ except: { refs: [keyword] }
+ }
+ end
- context 'using both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: [keyword] },
- except: { refs: [keyword] }
- }
+ it { is_expected.not_to be_included }
end
-
- it { is_expected.not_to be_included }
end
end
end
- end
- context 'when repository path matches' do
- context 'when using only' do
- let(:attributes) do
- { name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] } }
+ context 'when repository path matches' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] } }
+ end
+
+ it { is_expected.to be_included }
end
- it { is_expected.to be_included }
- end
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: ["branches@#{pipeline.project_full_path}"] } }
+ end
- context 'when using except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: ["branches@#{pipeline.project_full_path}"] } }
+ it { is_expected.not_to be_included }
end
- it { is_expected.not_to be_included }
- end
+ context 'when using both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: ["branches@#{pipeline.project_full_path}"] },
+ except: { refs: ["branches@#{pipeline.project_full_path}"] }
+ }
+ end
- context 'when using both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: ["branches@#{pipeline.project_full_path}"] },
- except: { refs: ["branches@#{pipeline.project_full_path}"] }
- }
+ it { is_expected.not_to be_included }
end
- it { is_expected.not_to be_included }
- end
-
- context 'when using both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: {
- refs: ["branches@#{pipeline.project_full_path}"]
- },
- except: {
- refs: ["branches@#{pipeline.project_full_path}"]
+ context 'when using both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: {
+ refs: ["branches@#{pipeline.project_full_path}"]
+ },
+ except: {
+ refs: ["branches@#{pipeline.project_full_path}"]
+ }
}
- }
- end
+ end
- it { is_expected.not_to be_included }
+ it { is_expected.not_to be_included }
+ end
end
- end
- context 'when repository path does not match' do
- context 'when using only' do
- let(:attributes) do
- { name: 'rspec', only: { refs: %w[branches@fork] } }
+ context 'when repository path does not match' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: %w[branches@fork] } }
+ end
+
+ it { is_expected.not_to be_included }
end
- it { is_expected.not_to be_included }
- end
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: %w[branches@fork] } }
+ end
- context 'when using except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: %w[branches@fork] } }
+ it { is_expected.to be_included }
end
- it { is_expected.to be_included }
- end
+ context 'when using both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: %w[branches@fork] },
+ except: { refs: %w[branches@fork] }
+ }
+ end
- context 'when using both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: %w[branches@fork] },
- except: { refs: %w[branches@fork] }
- }
+ it { is_expected.not_to be_included }
end
-
- it { is_expected.not_to be_included }
end
- end
- context 'using rules:' do
- using RSpec::Parameterized
+ context 'using rules:' do
+ using RSpec::Parameterized
- let(:attributes) { { name: 'rspec', rules: rule_set, when: 'on_success' } }
+ let(:attributes) { { name: 'rspec', rules: rule_set, when: 'on_success' } }
- context 'with a matching if: rule' do
- context 'with an explicit `when: never`' do
- where(:rule_set) do
- [
- [[{ if: '$VARIABLE == null', when: 'never' }]],
- [[{ if: '$VARIABLE == null', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]],
- [[{ if: '$VARIABLE != "the wrong value"', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]]
- ]
- end
+ context 'with a matching if: rule' do
+ context 'with an explicit `when: never`' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE == null', when: 'never' }]],
+ [[{ if: '$VARIABLE == null', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]],
+ [[{ if: '$VARIABLE != "the wrong value"', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]]
+ ]
+ end
- with_them do
- it { is_expected.not_to be_included }
+ with_them do
+ it { is_expected.not_to be_included }
- it 'still correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'never')
+ it 'still correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
end
end
- end
- context 'with an explicit `when: always`' do
- where(:rule_set) do
- [
- [[{ if: '$VARIABLE == null', when: 'always' }]],
- [[{ if: '$VARIABLE == null', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]],
- [[{ if: '$VARIABLE != "the wrong value"', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]]
- ]
+ context 'with an explicit `when: always`' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE == null', when: 'always' }]],
+ [[{ if: '$VARIABLE == null', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]],
+ [[{ if: '$VARIABLE != "the wrong value"', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'always')
+ end
+ end
end
- with_them do
- it { is_expected.to be_included }
+ context 'with an explicit `when: on_failure`' do
+ where(:rule_set) do
+ [
+ [[{ if: '$CI_JOB_NAME == "rspec" && $VAR == null', when: 'on_failure' }]],
+ [[{ if: '$VARIABLE != null', when: 'delayed', start_in: '1 day' }, { if: '$CI_JOB_NAME == "rspec"', when: 'on_failure' }]],
+ [[{ if: '$VARIABLE == "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$CI_BUILD_NAME == "rspec"', when: 'on_failure' }]]
+ ]
+ end
- it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'always')
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'on_failure')
+ end
end
end
- end
- context 'with an explicit `when: on_failure`' do
- where(:rule_set) do
- [
- [[{ if: '$CI_JOB_NAME == "rspec" && $VAR == null', when: 'on_failure' }]],
- [[{ if: '$VARIABLE != null', when: 'delayed', start_in: '1 day' }, { if: '$CI_JOB_NAME == "rspec"', when: 'on_failure' }]],
- [[{ if: '$VARIABLE == "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$CI_BUILD_NAME == "rspec"', when: 'on_failure' }]]
- ]
+ context 'with an explicit `when: delayed`' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }]],
+ [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]],
+ [[{ if: '$VARIABLE != "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'delayed', options: { start_in: '1 day' })
+ end
+ end
end
- with_them do
- it { is_expected.to be_included }
+ context 'without an explicit when: value' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE == null' }]],
+ [[{ if: '$VARIABLE == null' }, { if: '$VARIABLE == null' }]],
+ [[{ if: '$VARIABLE != "the wrong value"' }, { if: '$VARIABLE == null' }]]
+ ]
+ end
- it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'on_failure')
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'on_success')
+ end
end
end
end
- context 'with an explicit `when: delayed`' do
- where(:rule_set) do
- [
- [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }]],
- [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]],
- [[{ if: '$VARIABLE != "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]]
- ]
+ context 'with a matching changes: rule' do
+ let(:pipeline) do
+ build(:ci_pipeline, project: project).tap do |pipeline|
+ stub_pipeline_modified_paths(pipeline, %w[app/models/ci/pipeline.rb spec/models/ci/pipeline_spec.rb .gitlab-ci.yml])
+ end
end
- with_them do
- it { is_expected.to be_included }
+ context 'with an explicit `when: never`' do
+ where(:rule_set) do
+ [
+ [[{ changes: { paths: %w[*/**/*.rb] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb] }, when: 'always' }]],
+ [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }]],
+ [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'never' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'always' }]],
+ [[{ changes: { paths: %w[*.yml] }, when: 'never' }, { changes: { paths: %w[*.yml] }, when: 'always' }]],
+ [[{ changes: { paths: %w[.*.yml] }, when: 'never' }, { changes: { paths: %w[.*.yml] }, when: 'always' }]],
+ [[{ changes: { paths: %w[**/*] }, when: 'never' }, { changes: { paths: %w[**/*] }, when: 'always' }]],
+ [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }]],
+ [[{ changes: { paths: %w[.*.yml **/*] }, when: 'never' }, { changes: { paths: %w[.*.yml **/*] }, when: 'always' }]]
+ ]
+ end
- it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'delayed', options: { start_in: '1 day' })
+ with_them do
+ it { is_expected.not_to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
end
end
- end
- context 'without an explicit when: value' do
- where(:rule_set) do
- [
- [[{ if: '$VARIABLE == null' }]],
- [[{ if: '$VARIABLE == null' }, { if: '$VARIABLE == null' }]],
- [[{ if: '$VARIABLE != "the wrong value"' }, { if: '$VARIABLE == null' }]]
- ]
- end
+ context 'with an explicit `when: always`' do
+ where(:rule_set) do
+ [
+ [[{ changes: { paths: %w[*/**/*.rb] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb] }, when: 'never' }]],
+ [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }]],
+ [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'always' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'never' }]],
+ [[{ changes: { paths: %w[*.yml] }, when: 'always' }, { changes: { paths: %w[*.yml] }, when: 'never' }]],
+ [[{ changes: { paths: %w[.*.yml] }, when: 'always' }, { changes: { paths: %w[.*.yml] }, when: 'never' }]],
+ [[{ changes: { paths: %w[**/*] }, when: 'always' }, { changes: { paths: %w[**/*] }, when: 'never' }]],
+ [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }]],
+ [[{ changes: { paths: %w[.*.yml **/*] }, when: 'always' }, { changes: { paths: %w[.*.yml **/*] }, when: 'never' }]]
+ ]
+ end
- with_them do
- it { is_expected.to be_included }
+ with_them do
+ it { is_expected.to be_included }
- it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'on_success')
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'always')
+ end
end
end
- end
- end
- context 'with a matching changes: rule' do
- let(:pipeline) do
- build(:ci_pipeline, project: project).tap do |pipeline|
- stub_pipeline_modified_paths(pipeline, %w[app/models/ci/pipeline.rb spec/models/ci/pipeline_spec.rb .gitlab-ci.yml])
+ context 'without an explicit when: value' do
+ where(:rule_set) do
+ [
+ [[{ changes: { paths: %w[*/**/*.rb] } }]],
+ [[{ changes: { paths: %w[app/models/ci/pipeline.rb] } }]],
+ [[{ changes: { paths: %w[spec/**/*.rb] } }]],
+ [[{ changes: { paths: %w[*.yml] } }]],
+ [[{ changes: { paths: %w[.*.yml] } }]],
+ [[{ changes: { paths: %w[**/*] } }]],
+ [[{ changes: { paths: %w[*/**/*.rb *.yml] } }]],
+ [[{ changes: { paths: %w[.*.yml **/*] } }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'on_success')
+ end
+ end
end
end
- context 'with an explicit `when: never`' do
+ context 'with no matching rule' do
where(:rule_set) do
[
- [[{ changes: { paths: %w[*/**/*.rb] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb] }, when: 'always' }]],
- [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }]],
- [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'never' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'always' }]],
- [[{ changes: { paths: %w[*.yml] }, when: 'never' }, { changes: { paths: %w[*.yml] }, when: 'always' }]],
- [[{ changes: { paths: %w[.*.yml] }, when: 'never' }, { changes: { paths: %w[.*.yml] }, when: 'always' }]],
- [[{ changes: { paths: %w[**/*] }, when: 'never' }, { changes: { paths: %w[**/*] }, when: 'always' }]],
- [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }]],
- [[{ changes: { paths: %w[.*.yml **/*] }, when: 'never' }, { changes: { paths: %w[.*.yml **/*] }, when: 'always' }]]
+ [[{ if: '$VARIABLE != null', when: 'never' }]],
+ [[{ if: '$VARIABLE != null', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]],
+ [[{ if: '$VARIABLE == "the wrong value"', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]],
+ [[{ if: '$VARIABLE != null', when: 'always' }]],
+ [[{ if: '$VARIABLE != null', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]],
+ [[{ if: '$VARIABLE == "the wrong value"', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]],
+ [[{ if: '$VARIABLE != null' }]],
+ [[{ if: '$VARIABLE != null' }, { if: '$VARIABLE != null' }]],
+ [[{ if: '$VARIABLE == "the wrong value"' }, { if: '$VARIABLE != null' }]]
]
end
@@ -878,257 +971,249 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
- context 'with an explicit `when: always`' do
- where(:rule_set) do
- [
- [[{ changes: { paths: %w[*/**/*.rb] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb] }, when: 'never' }]],
- [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }]],
- [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'always' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'never' }]],
- [[{ changes: { paths: %w[*.yml] }, when: 'always' }, { changes: { paths: %w[*.yml] }, when: 'never' }]],
- [[{ changes: { paths: %w[.*.yml] }, when: 'always' }, { changes: { paths: %w[.*.yml] }, when: 'never' }]],
- [[{ changes: { paths: %w[**/*] }, when: 'always' }, { changes: { paths: %w[**/*] }, when: 'never' }]],
- [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }]],
- [[{ changes: { paths: %w[.*.yml **/*] }, when: 'always' }, { changes: { paths: %w[.*.yml **/*] }, when: 'never' }]]
- ]
+ context 'with a rule using CI_ENVIRONMENT_NAME variable' do
+ let(:rule_set) do
+ [{ if: '$CI_ENVIRONMENT_NAME == "test"' }]
end
- with_them do
+ context 'when environment:name satisfies the rule' do
+ let(:attributes) { { name: 'rspec', rules: rule_set, environment: 'test', when: 'on_success' } }
+
it { is_expected.to be_included }
it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'always')
+ expect(seed_build.attributes).to include(when: 'on_success')
end
end
- end
- context 'without an explicit when: value' do
- where(:rule_set) do
- [
- [[{ changes: { paths: %w[*/**/*.rb] } }]],
- [[{ changes: { paths: %w[app/models/ci/pipeline.rb] } }]],
- [[{ changes: { paths: %w[spec/**/*.rb] } }]],
- [[{ changes: { paths: %w[*.yml] } }]],
- [[{ changes: { paths: %w[.*.yml] } }]],
- [[{ changes: { paths: %w[**/*] } }]],
- [[{ changes: { paths: %w[*/**/*.rb *.yml] } }]],
- [[{ changes: { paths: %w[.*.yml **/*] } }]]
- ]
+ context 'when environment:name does not satisfy rule' do
+ let(:attributes) { { name: 'rspec', rules: rule_set, environment: 'dev', when: 'on_success' } }
+
+ it { is_expected.not_to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
end
- with_them do
- it { is_expected.to be_included }
+ context 'when environment:name is not set' do
+ it { is_expected.not_to be_included }
it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'on_success')
+ expect(seed_build.attributes).to include(when: 'never')
end
end
end
- end
- context 'with no matching rule' do
- where(:rule_set) do
- [
- [[{ if: '$VARIABLE != null', when: 'never' }]],
- [[{ if: '$VARIABLE != null', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]],
- [[{ if: '$VARIABLE == "the wrong value"', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]],
- [[{ if: '$VARIABLE != null', when: 'always' }]],
- [[{ if: '$VARIABLE != null', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]],
- [[{ if: '$VARIABLE == "the wrong value"', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]],
- [[{ if: '$VARIABLE != null' }]],
- [[{ if: '$VARIABLE != null' }, { if: '$VARIABLE != null' }]],
- [[{ if: '$VARIABLE == "the wrong value"' }, { if: '$VARIABLE != null' }]]
- ]
+ context 'with no rules' do
+ let(:rule_set) { [] }
+
+ it { is_expected.not_to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
end
- with_them do
+ context 'with invalid rules raising error' do
+ let(:rule_set) do
+ [
+ { changes: { paths: ['README.md'], compare_to: 'invalid-ref' }, when: 'never' }
+ ]
+ end
+
it { is_expected.not_to be_included }
it 'correctly populates when:' do
expect(seed_build.attributes).to include(when: 'never')
end
+
+ it 'returns an error' do
+ expect(seed_build.errors).to contain_exactly(
+ 'Failed to parse rule for rspec: rules:changes:compare_to is not a valid ref'
+ )
+ end
end
end
+ end
- context 'with no rules' do
- let(:rule_set) { [] }
+ describe 'applying needs: dependency' do
+ subject { seed_build }
- it { is_expected.not_to be_included }
+ let(:needs_count) { 1 }
- it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'never')
- end
+ let(:needs_attributes) do
+ Array.new(needs_count, name: 'build')
end
- context 'with invalid rules raising error' do
- let(:rule_set) do
- [
- { changes: { paths: ['README.md'], compare_to: 'invalid-ref' }, when: 'never' }
- ]
- end
+ let(:attributes) do
+ {
+ name: 'rspec',
+ needs_attributes: needs_attributes
+ }
+ end
- it { is_expected.not_to be_included }
+ context 'when build job is not present in prior stages' do
+ it "is included" do
+ is_expected.to be_included
+ end
- it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'never')
+ it "returns an error" do
+ expect(subject.errors).to contain_exactly(
+ "'rspec' job needs 'build' job, but 'build' is not in any previous stage")
end
- it 'returns an error' do
- expect(seed_build.errors).to contain_exactly(
- 'Failed to parse rule for rspec: rules:changes:compare_to is not a valid ref'
- )
+ context 'when the needed job is optional' do
+ let(:needs_attributes) { [{ name: 'build', optional: true }] }
+
+ it "does not return an error" do
+ expect(subject.errors).to be_empty
+ end
end
end
- end
- end
- describe 'applying needs: dependency' do
- subject { seed_build }
+ context 'when build job is part of prior stages' do
+ let(:stage_attributes) do
+ {
+ name: 'build',
+ index: 0,
+ builds: [{ name: 'build' }]
+ }
+ end
- let(:needs_count) { 1 }
+ let(:stage_seed) do
+ Gitlab::Ci::Pipeline::Seed::Stage.new(seed_context, stage_attributes, [])
+ end
- let(:needs_attributes) do
- Array.new(needs_count, name: 'build')
- end
+ let(:previous_stages) { [stage_seed] }
- let(:attributes) do
- {
- name: 'rspec',
- needs_attributes: needs_attributes
- }
- end
+ it "is included" do
+ is_expected.to be_included
+ end
- context 'when build job is not present in prior stages' do
- it "is included" do
- is_expected.to be_included
+ it "does not have errors" do
+ expect(subject.errors).to be_empty
+ end
end
- it "returns an error" do
- expect(subject.errors).to contain_exactly(
- "'rspec' job needs 'build' job, but 'build' is not in any previous stage")
- end
+ context 'when build job is part of the same stage' do
+ let(:current_stage) { double(seeds_names: [attributes[:name], 'build']) }
- context 'when the needed job is optional' do
- let(:needs_attributes) { [{ name: 'build', optional: true }] }
+ it 'is included' do
+ is_expected.to be_included
+ end
- it "does not return an error" do
+ it 'does not have errors' do
expect(subject.errors).to be_empty
end
end
- end
-
- context 'when build job is part of prior stages' do
- let(:stage_attributes) do
- {
- name: 'build',
- index: 0,
- builds: [{ name: 'build' }]
- }
- end
-
- let(:stage_seed) do
- Gitlab::Ci::Pipeline::Seed::Stage.new(seed_context, stage_attributes, [])
- end
- let(:previous_stages) { [stage_seed] }
+ context 'when using 101 needs' do
+ let(:needs_count) { 101 }
- it "is included" do
- is_expected.to be_included
- end
+ it "returns an error" do
+ expect(subject.errors).to contain_exactly(
+ "rspec: one job can only need 50 others, but you have listed 101. See needs keyword documentation for more details")
+ end
- it "does not have errors" do
- expect(subject.errors).to be_empty
- end
- end
+ context 'when ci_needs_size_limit is set to 100' do
+ before do
+ project.actual_limits.update!(ci_needs_size_limit: 100)
+ end
- context 'when build job is part of the same stage' do
- let(:current_stage) { double(seeds_names: [attributes[:name], 'build']) }
+ it "returns an error" do
+ expect(subject.errors).to contain_exactly(
+ "rspec: one job can only need 100 others, but you have listed 101. See needs keyword documentation for more details")
+ end
+ end
- it 'is included' do
- is_expected.to be_included
- end
+ context 'when ci_needs_size_limit is set to 0' do
+ before do
+ project.actual_limits.update!(ci_needs_size_limit: 0)
+ end
- it 'does not have errors' do
- expect(subject.errors).to be_empty
+ it "returns an error" do
+ expect(subject.errors).to contain_exactly(
+ "rspec: one job can only need 0 others, but you have listed 101. See needs keyword documentation for more details")
+ end
+ end
end
end
- context 'when using 101 needs' do
- let(:needs_count) { 101 }
+ describe 'applying pipeline variables' do
+ subject { seed_build }
- it "returns an error" do
- expect(subject.errors).to contain_exactly(
- "rspec: one job can only need 50 others, but you have listed 101. See needs keyword documentation for more details")
+ let(:pipeline_variables) { [] }
+ let(:pipeline) do
+ build(:ci_empty_pipeline, project: project, sha: head_sha, variables: pipeline_variables)
end
- context 'when ci_needs_size_limit is set to 100' do
- before do
- project.actual_limits.update!(ci_needs_size_limit: 100)
+ context 'containing variable references' do
+ let(:pipeline_variables) do
+ [
+ build(:ci_pipeline_variable, key: 'A', value: '$B'),
+ build(:ci_pipeline_variable, key: 'B', value: '$C')
+ ]
end
- it "returns an error" do
- expect(subject.errors).to contain_exactly(
- "rspec: one job can only need 100 others, but you have listed 101. See needs keyword documentation for more details")
+ it "does not have errors" do
+ expect(subject.errors).to be_empty
end
end
- context 'when ci_needs_size_limit is set to 0' do
- before do
- project.actual_limits.update!(ci_needs_size_limit: 0)
+ context 'containing cyclic reference' do
+ let(:pipeline_variables) do
+ [
+ build(:ci_pipeline_variable, key: 'A', value: '$B'),
+ build(:ci_pipeline_variable, key: 'B', value: '$C'),
+ build(:ci_pipeline_variable, key: 'C', value: '$A')
+ ]
end
it "returns an error" do
expect(subject.errors).to contain_exactly(
- "rspec: one job can only need 0 others, but you have listed 101. See needs keyword documentation for more details")
+ 'rspec: circular variable reference detected: ["A", "B", "C"]')
+ end
+
+ context 'with job:rules:[if:]' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } }
+
+ it "included? does not raise" do
+ expect { subject.included? }.not_to raise_error
+ end
+
+ it "included? returns true" do
+ expect(subject.included?).to eq(true)
+ end
end
end
end
end
- describe 'applying pipeline variables' do
- subject { seed_build }
-
- let(:pipeline_variables) { [] }
- let(:pipeline) do
- build(:ci_empty_pipeline, project: project, sha: head_sha, variables: pipeline_variables)
+ describe 'feature flag ci_reuse_build_in_seed_context' do
+ let(:attributes) do
+ { name: 'rspec', rules: [{ if: '$VARIABLE == null' }], when: 'on_success' }
end
- context 'containing variable references' do
- let(:pipeline_variables) do
- [
- build(:ci_pipeline_variable, key: 'A', value: '$B'),
- build(:ci_pipeline_variable, key: 'B', value: '$C')
- ]
- end
+ context 'when enabled' do
+ it_behaves_like 'build seed'
- it "does not have errors" do
- expect(subject.errors).to be_empty
+ it 'initializes the build once' do
+ expect(Ci::Build).to receive(:new).once.and_call_original
+ seed_build.to_resource
end
end
- context 'containing cyclic reference' do
- let(:pipeline_variables) do
- [
- build(:ci_pipeline_variable, key: 'A', value: '$B'),
- build(:ci_pipeline_variable, key: 'B', value: '$C'),
- build(:ci_pipeline_variable, key: 'C', value: '$A')
- ]
- end
-
- it "returns an error" do
- expect(subject.errors).to contain_exactly(
- 'rspec: circular variable reference detected: ["A", "B", "C"]')
+ context 'when disabled' do
+ before do
+ stub_feature_flags(ci_reuse_build_in_seed_context: false)
end
- context 'with job:rules:[if:]' do
- let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } }
-
- it "included? does not raise" do
- expect { subject.included? }.not_to raise_error
- end
+ it_behaves_like 'build seed'
- it "included? returns true" do
- expect(subject.included?).to eq(true)
- end
+ it 'initializes the build twice' do
+ expect(Ci::Build).to receive(:new).twice.and_call_original
+ seed_build.to_resource
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
index a632b5dedcf..288ac3f3854 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Seed::Stage do
+RSpec.describe Gitlab::Ci::Pipeline::Seed::Stage, feature_category: :pipeline_authoring do
let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:previous_stages) { [] }
diff --git a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
index cdaf9354104..5dbcc1991d4 100644
--- a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Reports::Sbom::Component do
+RSpec.describe Gitlab::Ci::Reports::Sbom::Component, feature_category: :dependency_management do
let(:component_type) { 'library' }
let(:name) { 'component-name' }
let(:purl_type) { 'npm' }
diff --git a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb
index f9a83378f46..5d281f6ed76 100644
--- a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Reports::Sbom::Report do
+RSpec.describe Gitlab::Ci::Reports::Sbom::Report, feature_category: :dependency_management do
subject(:report) { described_class.new }
describe '#valid?' do
diff --git a/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb b/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb
index 75ea91251eb..4fb766d7d38 100644
--- a/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Ci::Reports::Sbom::Reports do
+RSpec.describe Gitlab::Ci::Reports::Sbom::Reports, feature_category: :dependency_management do
subject(:reports_list) { described_class.new }
describe '#add_report' do
diff --git a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb
index 343c0d8c15c..63b8e5fdf01 100644
--- a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Ci::Reports::Sbom::Source do
+RSpec.describe Gitlab::Ci::Reports::Sbom::Source, feature_category: :dependency_management do
let(:attributes) do
{
type: :dependency_scanning,
diff --git a/spec/lib/gitlab/ci/reports/security/reports_spec.rb b/spec/lib/gitlab/ci/reports/security/reports_spec.rb
index 33f3317c655..cb6a91655ed 100644
--- a/spec/lib/gitlab/ci/reports/security/reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/security/reports_spec.rb
@@ -52,105 +52,4 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do
it { is_expected.to match_array(expected_findings) }
end
-
- describe "#violates_default_policy_against?" do
- let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: 'dast') }
- let(:vulnerabilities_allowed) { 0 }
- let(:severity_levels) { %w(critical high) }
- let(:vulnerability_states) { %w(newly_detected) }
-
- subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states) }
-
- before do
- security_reports.get_report('sast', artifact).add_finding(high_severity_dast)
- end
-
- context 'when the target_reports is `nil`' do
- let(:target_reports) { nil }
-
- context 'with severity levels matching the existing vulnerabilities' do
- it { is_expected.to be(true) }
- end
-
- context "without any severity levels matching the existing vulnerabilities" do
- let(:severity_levels) { %w(critical) }
-
- it { is_expected.to be(false) }
- end
- end
-
- context 'when the target_reports is not `nil`' do
- let(:target_reports) { described_class.new(pipeline) }
-
- context "when a report has a new unsafe vulnerability" do
- context 'with severity levels matching the existing vulnerabilities' do
- it { is_expected.to be(true) }
- end
-
- it { is_expected.to be(true) }
-
- context 'with vulnerabilities_allowed higher than the number of new vulnerabilities' do
- let(:vulnerabilities_allowed) { 10000 }
-
- it { is_expected.to be(false) }
- end
-
- context "without any severity levels matching the existing vulnerabilities" do
- let(:severity_levels) { %w(critical) }
-
- it { is_expected.to be(false) }
- end
- end
-
- context "when none of the reports have a new unsafe vulnerability" do
- before do
- target_reports.get_report('sast', artifact).add_finding(high_severity_dast)
- end
-
- it { is_expected.to be(false) }
- end
-
- context 'with related report_types' do
- let(:report_types) { %w(dast sast) }
-
- subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states, report_types) }
-
- it { is_expected.to be(true) }
- end
-
- context 'with unrelated report_types' do
- let(:report_types) { %w(dependency_scanning sast) }
-
- subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states, report_types) }
-
- it { is_expected.to be(false) }
- end
-
- context 'when target_reports is not nil and reports is empty' do
- let(:without_reports) { described_class.new(pipeline) }
-
- subject { without_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states) }
-
- before do
- target_reports.get_report('sast', artifact).add_finding(high_severity_dast)
- end
-
- context 'when require_approval_on_scan_removal feature is enabled' do
- before do
- stub_feature_flags(require_approval_on_scan_removal: true)
- end
-
- it { is_expected.to be(true) }
- end
-
- context 'when require_approval_on_scan_removal feature is disabled' do
- before do
- stub_feature_flags(require_approval_on_scan_removal: false)
- end
-
- it { is_expected.to be(false) }
- end
- end
- end
- end
end
diff --git a/spec/lib/gitlab/ci/runner_instructions_spec.rb b/spec/lib/gitlab/ci/runner_instructions_spec.rb
index f872c631a50..56f69720b87 100644
--- a/spec/lib/gitlab/ci/runner_instructions_spec.rb
+++ b/spec/lib/gitlab/ci/runner_instructions_spec.rb
@@ -69,6 +69,7 @@ RSpec.describe Gitlab::Ci::RunnerInstructions do
'windows' | 'amd64'
'windows' | '386'
'osx' | 'amd64'
+ 'osx' | 'arm64'
end
with_them do
diff --git a/spec/lib/gitlab/ci/trace/archive_spec.rb b/spec/lib/gitlab/ci/trace/archive_spec.rb
index f91cb03883a..582c4ad343f 100644
--- a/spec/lib/gitlab/ci/trace/archive_spec.rb
+++ b/spec/lib/gitlab/ci/trace/archive_spec.rb
@@ -75,15 +75,6 @@ RSpec.describe Gitlab::Ci::Trace::Archive do
include_context 'with FIPS'
end
- context 'with background_upload enabled' do
- before do
- stub_artifacts_object_storage(background_upload: true)
- end
-
- it_behaves_like 'skips validations'
- include_context 'with FIPS'
- end
-
context 'with direct_upload enabled' do
before do
stub_artifacts_object_storage(direct_upload: true)
diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb
index 52ba85d2df1..5aa752ee429 100644
--- a/spec/lib/gitlab/ci/variables/builder_spec.rb
+++ b/spec/lib/gitlab/ci/variables/builder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, feature_category: :pipeline_authoring do
include Ci::TemplateHelpers
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, namespace: group) }
@@ -13,7 +13,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do
name: 'rspec:test 1',
pipeline: pipeline,
user: user,
- yaml_variables: [{ key: 'YAML_VARIABLE', value: 'value' }]
+ yaml_variables: [{ key: 'YAML_VARIABLE', value: 'value' }],
+ environment: 'test'
)
end
@@ -32,6 +33,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do
value: job.stage_name },
{ key: 'CI_NODE_TOTAL',
value: '1' },
+ { key: 'CI_ENVIRONMENT_NAME',
+ value: 'test' },
{ key: 'CI_BUILD_NAME',
value: 'rspec:test 1' },
{ key: 'CI_BUILD_STAGE',
@@ -76,6 +79,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do
value: project.full_path_slug },
{ key: 'CI_PROJECT_NAMESPACE',
value: project.namespace.full_path },
+ { key: 'CI_PROJECT_NAMESPACE_ID',
+ value: project.namespace.id.to_s },
{ key: 'CI_PROJECT_ROOT_NAMESPACE',
value: project.namespace.root_ancestor.path },
{ key: 'CI_PROJECT_URL',
@@ -276,11 +281,17 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do
subject { builder.kubernetes_variables(environment: nil, job: job) }
before do
- allow(Ci::GenerateKubeconfigService).to receive(:new).with(job.pipeline, token: job.token).and_return(service)
+ allow(Ci::GenerateKubeconfigService).to receive(:new).with(job.pipeline, token: job.token, environment: anything).and_return(service)
end
it { is_expected.to include(key: 'KUBECONFIG', value: 'example-kubeconfig', public: false, file: true) }
+ it 'calls the GenerateKubeconfigService with the correct arguments' do
+ expect(Ci::GenerateKubeconfigService).to receive(:new).with(job.pipeline, token: job.token, environment: nil)
+
+ subject
+ end
+
context 'generated config is invalid' do
let(:template_valid) { false }
@@ -297,6 +308,16 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do
expect(subject['KUBECONFIG'].value).to eq('example-kubeconfig')
expect(subject['OTHER'].value).to eq('some value')
end
+
+ context 'when environment is not nil' do
+ subject { builder.kubernetes_variables(environment: 'production', job: job) }
+
+ it 'passes the environment when generating the KUBECONFIG' do
+ expect(Ci::GenerateKubeconfigService).to receive(:new).with(job.pipeline, token: job.token, environment: 'production')
+
+ subject
+ end
+ end
end
describe '#deployment_variables' do
diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
index 7f203168706..5c9f156e054 100644
--- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
@@ -12,6 +12,20 @@ module Gitlab
let(:ci_config) { Gitlab::Ci::Config.new(config_content, user: user) }
let(:result) { described_class.new(ci_config: ci_config, warnings: ci_config&.warnings) }
+ describe '#builds' do
+ context 'when a job has ID tokens' do
+ let(:config_content) do
+ YAML.dump(
+ test: { stage: 'test', script: 'echo', id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } } }
+ )
+ end
+
+ it 'includes `id_tokens`' do
+ expect(result.builds.first[:id_tokens]).to eq({ TEST_ID_TOKEN: { aud: 'https://gitlab.com' } })
+ end
+ end
+ end
+
describe '#config_metadata' do
subject(:config_metadata) { result.config_metadata }
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 5de813f7739..ae98d2e0cad 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -870,6 +870,69 @@ module Gitlab
end
end
end
+
+ describe "hooks" do
+ context 'when it is a simple script' do
+ let(:config) do
+ {
+ test: { script: ["script"],
+ hooks: { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] } }
+ }
+ end
+
+ it "returns hooks in options" do
+ expect(subject[:options][:hooks]).to eq(
+ { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] }
+ )
+ end
+ end
+
+ context 'when it is nested arrays of strings' do
+ let(:config) do
+ {
+ test: { script: ["script"],
+ hooks: { pre_get_sources_script: [[["global script"], "echo 1"], "echo 2", ["ls"], "pwd"] } }
+ }
+ end
+
+ it "returns hooks in options" do
+ expect(subject[:options][:hooks]).to eq(
+ { pre_get_sources_script: ["global script", "echo 1", "echo 2", "ls", "pwd"] }
+ )
+ end
+ end
+
+ context 'when receiving from the default' do
+ let(:config) do
+ {
+ default: { hooks: { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] } },
+ test: { script: ["script"] }
+ }
+ end
+
+ it "inherits hooks" do
+ expect(subject[:options][:hooks]).to eq(
+ { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] }
+ )
+ end
+ end
+
+ context 'when overriding the default' do
+ let(:config) do
+ {
+ default: { hooks: { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] } },
+ test: { script: ["script"],
+ hooks: { pre_get_sources_script: ["echo 3", "echo 4", "pwd"] } }
+ }
+ end
+
+ it "overrides hooks" do
+ expect(subject[:options][:hooks]).to eq(
+ { pre_get_sources_script: ["echo 3", "echo 4", "pwd"] }
+ )
+ end
+ end
+ end
end
describe "Image and service handling" do
@@ -2883,7 +2946,7 @@ module Gitlab
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 } } }) }
- it_behaves_like 'returns errors', 'jobs:rspec:artifacts when should be on_success, on_failure or always'
+ 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
diff --git a/spec/lib/gitlab/cluster/rack_timeout_observer_spec.rb b/spec/lib/gitlab/cluster/rack_timeout_observer_spec.rb
index 05df4089075..2d4a7a3b170 100644
--- a/spec/lib/gitlab/cluster/rack_timeout_observer_spec.rb
+++ b/spec/lib/gitlab/cluster/rack_timeout_observer_spec.rb
@@ -73,5 +73,28 @@ RSpec.describe Gitlab::Cluster::RackTimeoutObserver do
subject.callback.call(env)
end
end
+
+ context 'when request contains invalid string' do
+ let(:env) do
+ {
+ ::Rack::Timeout::ENV_INFO_KEY => double(state: :timed_out),
+ 'action_dispatch.request.parameters' => {
+ 'controller' => 'foo',
+ 'action' => '\u003c',
+ 'route' => '?8\u003c/x'
+ }
+ }
+ end
+
+ subject { described_class.new }
+
+ it 'sanitizes string' do
+ expect(counter)
+ .to receive(:increment)
+ .with({ controller: 'foo', action: '\\u003c', route: '?8\\u003c/x', state: :timed_out })
+
+ subject.callback.call(env)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/config/entry/attributable_spec.rb b/spec/lib/gitlab/config/entry/attributable_spec.rb
index 8a207bddaae..0a2f8ac2c3a 100644
--- a/spec/lib/gitlab/config/entry/attributable_spec.rb
+++ b/spec/lib/gitlab/config/entry/attributable_spec.rb
@@ -10,10 +10,11 @@ RSpec.describe Gitlab::Config::Entry::Attributable do
end
let(:instance) { node.new }
+ let(:prefix) { nil }
before do
- node.class_eval do
- attributes :name, :test
+ node.class_exec(prefix) do |pre|
+ attributes :name, :test, prefix: pre
end
end
@@ -24,6 +25,17 @@ RSpec.describe Gitlab::Config::Entry::Attributable do
.and_return({ name: 'some name', test: 'some test' })
end
+ context 'and is provided a prefix' do
+ let(:prefix) { :pre }
+
+ it 'returns the value of config' do
+ expect(instance).to have_pre_name
+ expect(instance.pre_name).to eq 'some name'
+ expect(instance).to have_pre_test
+ expect(instance.pre_test).to eq 'some test'
+ end
+ end
+
it 'returns the value of config' do
expect(instance).to have_name
expect(instance.name).to eq 'some name'
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index 165305476d2..6ea8e6c6706 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::Conflict::File do
let(:section_keys) { conflict_file.sections.map { |section| section[:id] }.compact }
context 'when resolving everything to the same side' do
- let(:resolution_hash) { section_keys.to_h { |key| [key, 'head'] } }
+ let(:resolution_hash) { section_keys.index_with { 'head' } }
let(:resolved_lines) { conflict_file.resolve_lines(resolution_hash) }
let(:expected_lines) { conflict_file.lines.reject { |line| line.type == 'old' } }
@@ -63,8 +63,8 @@ RSpec.describe Gitlab::Conflict::File do
end
it 'raises ResolutionError when passed a hash without resolutions for all sections' do
- empty_hash = section_keys.to_h { |key| [key, nil] }
- invalid_hash = section_keys.to_h { |key| [key, 'invalid'] }
+ empty_hash = section_keys.index_with { nil }
+ invalid_hash = section_keys.index_with { 'invalid' }
expect { conflict_file.resolve_lines({}) }
.to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
index aadfb41a46e..88bffd41947 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -102,13 +102,65 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
context 'when sentry is configured' do
+ let(:legacy_dsn) { 'dummy://abc@legacy-sentry.example.com/1' }
+ let(:dsn) { 'dummy://def@sentry.example.com/2' }
+
before do
- stub_sentry_settings
stub_config_setting(host: 'gitlab.example.com')
end
- it 'adds sentry path to CSP without user' do
- expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://example.com")
+ context 'when legacy sentry is configured' do
+ before do
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return(legacy_dsn)
+ allow(Gitlab::CurrentSettings).to receive(:sentry_enabled).and_return(false)
+ end
+
+ it 'adds legacy sentry path to CSP' do
+ expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://legacy-sentry.example.com")
+ end
+ end
+
+ context 'when sentry is configured' do
+ before do
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(false)
+ allow(Gitlab::CurrentSettings).to receive(:sentry_enabled).and_return(true)
+ allow(Gitlab::CurrentSettings).to receive(:sentry_clientside_dsn).and_return(dsn)
+ end
+
+ it 'adds new sentry path to CSP' do
+ expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://sentry.example.com")
+ end
+ end
+
+ context 'when sentry settings are from older schemas and sentry setting are missing' do
+ before do
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(false)
+
+ allow(Gitlab::CurrentSettings).to receive(:respond_to?).with(:sentry_enabled).and_return(false)
+ allow(Gitlab::CurrentSettings).to receive(:sentry_enabled).and_raise(NoMethodError)
+
+ allow(Gitlab::CurrentSettings).to receive(:respond_to?).with(:sentry_clientside_dsn).and_return(false)
+ allow(Gitlab::CurrentSettings).to receive(:sentry_clientside_dsn).and_raise(NoMethodError)
+ end
+
+ it 'config is backwards compatible, does not add sentry path to CSP' do
+ expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com")
+ end
+ end
+
+ context 'when legacy sentry and sentry are both configured' do
+ before do
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return(legacy_dsn)
+
+ allow(Gitlab::CurrentSettings).to receive(:sentry_enabled).and_return(true)
+ allow(Gitlab::CurrentSettings).to receive(:sentry_clientside_dsn).and_return(dsn)
+ end
+
+ it 'adds both sentry paths to CSP' do
+ expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com dummy://legacy-sentry.example.com dummy://sentry.example.com")
+ end
end
end
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
index 8a9ab736d46..3736914669a 100644
--- a/spec/lib/gitlab/contributions_calendar_spec.rb
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -2,24 +2,24 @@
require 'spec_helper'
-RSpec.describe Gitlab::ContributionsCalendar do
- let(:contributor) { create(:user) }
- let(:user) { create(:user) }
+RSpec.describe Gitlab::ContributionsCalendar, feature_category: :users do
+ let_it_be_with_reload(:contributor) { create(:user) }
+ let_it_be_with_reload(:user) { create(:user) }
let(:travel_time) { nil }
- let(:private_project) do
+ let_it_be_with_reload(:private_project) do
create(:project, :private) do |project|
create(:project_member, user: contributor, project: project)
end
end
- let(:public_project) do
+ let_it_be(:public_project) do
create(:project, :public, :repository) do |project|
create(:project_member, user: contributor, project: project)
end
end
- let(:feature_project) do
+ let_it_be(:feature_project) do
create(:project, :public, :issues_private) do |project|
create(:project_member, user: contributor, project: project).project
end
@@ -30,6 +30,7 @@ RSpec.describe Gitlab::ContributionsCalendar do
let(:tomorrow) { today + 1.day }
let(:last_week) { today - 7.days }
let(:last_year) { today - 1.year }
+ let(:targets) { {} }
before do
travel_to travel_time || Time.now.utc.end_of_day
@@ -44,26 +45,28 @@ RSpec.describe Gitlab::ContributionsCalendar do
end
def create_event(project, day, hour = 0, action = :created, target_symbol = :issue)
- @targets ||= {}
- @targets[project] ||= create(target_symbol, project: project, author: contributor)
+ targets[project] ||= create(target_symbol, project: project, author: contributor)
Event.create!(
project: project,
action: action,
- target_type: @targets[project].class.name,
- target_id: @targets[project].id,
+ target_type: targets[project].class.name,
+ target_id: targets[project].id,
author: contributor,
created_at: DateTime.new(day.year, day.month, day.day, hour)
)
end
- describe '#activity_dates' do
+ describe '#activity_dates', :aggregate_failures do
it "returns a hash of date => count" do
create_event(public_project, last_week)
create_event(public_project, last_week)
create_event(public_project, today)
+ work_item_event = create_event(private_project, today, 0, :created, :work_item)
- expect(calendar.activity_dates).to eq(last_week => 2, today => 1)
+ # make sure the target is a work item as we want to include those in the count
+ expect(work_item_event.target_type).to eq('WorkItem')
+ expect(calendar(contributor).activity_dates).to eq(last_week => 2, today => 2)
end
context "when the user has opted-in for private contributions" do
@@ -176,9 +179,11 @@ RSpec.describe Gitlab::ContributionsCalendar do
it "returns all events for a given date" do
e1 = create_event(public_project, today)
e2 = create_event(public_project, today)
+ e3 = create_event(private_project, today, 0, :created, :work_item)
create_event(public_project, last_week)
- expect(calendar.events_by_date(today)).to contain_exactly(e1, e2)
+ expect([e1, e2, e3].map(&:target_type)).to contain_exactly('WorkItem', 'Issue', 'Issue')
+ expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3)
end
it "only shows private events to authorized users" do
diff --git a/spec/lib/gitlab/counters/buffered_counter_spec.rb b/spec/lib/gitlab/counters/buffered_counter_spec.rb
new file mode 100644
index 00000000000..a1fd97768ea
--- /dev/null
+++ b/spec/lib/gitlab/counters/buffered_counter_spec.rb
@@ -0,0 +1,233 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Counters::BufferedCounter, :clean_gitlab_redis_shared_state do
+ using RSpec::Parameterized::TableSyntax
+
+ subject(:counter) { described_class.new(counter_record, attribute) }
+
+ let(:counter_record) { create(:project_statistics) }
+ let(:attribute) { :build_artifacts_size }
+
+ describe '#get' do
+ it 'returns the value when there is an existing value stored in the counter' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(counter.key, 456)
+ end
+
+ expect(counter.get).to eq(456)
+ end
+
+ it 'returns 0 when there is no existing value' do
+ expect(counter.get).to eq(0)
+ end
+ end
+
+ describe '#increment' do
+ it 'sets a new key by the given value' do
+ counter.increment(123)
+
+ expect(counter.get).to eq(123)
+ end
+
+ it 'increments an existing key by the given value' do
+ counter.increment(100)
+ counter.increment(123)
+
+ expect(counter.get).to eq(100 + 123)
+ end
+
+ it 'returns the new value' do
+ counter.increment(123)
+
+ expect(counter.increment(23)).to eq(146)
+ end
+
+ it 'schedules a worker to commit the counter into database' do
+ expect(FlushCounterIncrementsWorker).to receive(:perform_in)
+ .with(described_class::WORKER_DELAY, counter_record.class.to_s, counter_record.id, attribute)
+
+ counter.increment(123)
+ end
+ end
+
+ describe '#reset!' do
+ before do
+ allow(counter_record).to receive(:update!)
+
+ counter.increment(123)
+ end
+
+ it 'removes the key from Redis' do
+ counter.reset!
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.exists?(counter.key)).to eq(false)
+ end
+ end
+
+ it 'resets the counter to 0' do
+ counter.reset!
+
+ expect(counter.get).to eq(0)
+ end
+
+ it 'resets the record to 0' do
+ expect(counter_record).to receive(:update!).with(attribute => 0)
+
+ counter.reset!
+ end
+ end
+
+ describe '#commit_increment!' do
+ it 'obtains an exclusive lease during processing' do
+ expect(counter).to receive(:with_exclusive_lease).and_call_original
+
+ counter.commit_increment!
+ end
+
+ context 'when there is an amount to commit' do
+ let(:increments) { [10, -3] }
+
+ before do
+ increments.each { |i| counter.increment(i) }
+ end
+
+ it 'commits the increment into the database' do
+ expect { counter.commit_increment! }
+ .to change { counter_record.reset.read_attribute(attribute) }.by(increments.sum)
+ end
+
+ it 'removes the increment entry from Redis' do
+ Gitlab::Redis::SharedState.with do |redis|
+ key_exists = redis.exists?(counter.key)
+ expect(key_exists).to be_truthy
+ end
+
+ counter.commit_increment!
+
+ Gitlab::Redis::SharedState.with do |redis|
+ key_exists = redis.exists?(counter.key)
+ expect(key_exists).to be_falsey
+ end
+ end
+ end
+
+ context 'when there are no counters to flush' do
+ context 'when there are no counters in the relative :flushed key' do
+ it 'does not change the record' do
+ expect { counter.commit_increment! }.not_to change { counter_record.reset.attributes }
+ end
+ end
+
+ # This can be the case where updating counters in the database fails with error
+ # and retrying the worker will retry flushing the counters but the main key has
+ # disappeared and the increment has been moved to the "<...>:flushed" key.
+ context 'when there are counters in the relative :flushed key' do
+ let(:flushed_amount) { 10 }
+
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.incrby(counter.flushed_key, flushed_amount)
+ end
+ end
+
+ it 'updates the record' do
+ expect { counter.commit_increment! }
+ .to change { counter_record.reset.read_attribute(attribute) }.by(flushed_amount)
+ end
+
+ it 'deletes the relative :flushed key' do
+ counter.commit_increment!
+
+ Gitlab::Redis::SharedState.with do |redis|
+ key_exists = redis.exists?(counter.flushed_key)
+ expect(key_exists).to be_falsey
+ end
+ end
+ end
+
+ context 'when deleting :flushed key fails' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.incrby(counter.flushed_key, 10)
+
+ allow(redis).to receive(:del).and_raise('could not delete key')
+ end
+ end
+
+ it 'does a rollback of the counter update' do
+ expect { counter.commit_increment! }.to raise_error('could not delete key')
+
+ expect(counter_record.reset.read_attribute(attribute)).to eq(0)
+ end
+ end
+
+ context 'when the counter record has after_commit callbacks' do
+ it 'has registered callbacks' do
+ expect(counter_record.class.after_commit_callbacks.size).to eq(1)
+ end
+
+ context 'when there are increments to flush' do
+ before do
+ counter.increment(10)
+ end
+
+ it 'executes the callbacks' do
+ expect(counter_record).to receive(:execute_after_commit_callbacks).and_call_original
+
+ counter.commit_increment!
+ end
+ end
+
+ context 'when there are no increments to flush' do
+ it 'does not execute the callbacks' do
+ expect(counter_record).not_to receive(:execute_after_commit_callbacks).and_call_original
+
+ counter.commit_increment!
+ end
+ end
+ end
+ end
+ end
+
+ describe '#amount_to_be_flushed' do
+ let(:increment_key) { counter.key }
+ let(:flushed_key) { counter.flushed_key }
+
+ where(:increment, :flushed, :result, :flushed_key_present) do
+ nil | nil | 0 | false
+ nil | 0 | 0 | false
+ 0 | 0 | 0 | false
+ 1 | 0 | 1 | true
+ 1 | nil | 1 | true
+ 1 | 1 | 2 | true
+ 1 | -2 | -1 | true
+ -1 | 1 | 0 | false
+ end
+
+ with_them do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(increment_key, increment) if increment
+ redis.set(flushed_key, flushed) if flushed
+ end
+ end
+
+ it 'returns the current value to be flushed' do
+ value = counter.amount_to_be_flushed
+ expect(value).to eq(result)
+ end
+
+ it 'drops the increment key and creates the flushed key if it does not exist' do
+ counter.amount_to_be_flushed
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.exists?(increment_key)).to eq(false)
+ expect(redis.exists?(flushed_key)).to eq(flushed_key_present)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/counters/legacy_counter_spec.rb b/spec/lib/gitlab/counters/legacy_counter_spec.rb
new file mode 100644
index 00000000000..e66b1ce08c4
--- /dev/null
+++ b/spec/lib/gitlab/counters/legacy_counter_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Counters::LegacyCounter do
+ subject(:counter) { described_class.new(counter_record, attribute) }
+
+ let(:counter_record) { create(:project_statistics) }
+ let(:attribute) { :snippets_size }
+ let(:amount) { 123 }
+
+ describe '#increment' do
+ it 'increments the attribute in the counter record' do
+ expect { counter.increment(amount) }.to change { counter_record.reload.method(attribute).call }.by(amount)
+ end
+
+ it 'returns the value after the increment' do
+ counter.increment(100)
+
+ expect(counter.increment(amount)).to eq(100 + amount)
+ end
+
+ it 'executes after counter_record after commit callback' do
+ expect(counter_record).to receive(:execute_after_commit_callbacks).and_call_original
+
+ counter.increment(amount)
+ end
+ end
+
+ describe '#reset!' do
+ before do
+ allow(counter_record).to receive(:update!)
+ end
+
+ it 'resets the record to 0' do
+ expect(counter_record).to receive(:update!).with(attribute => 0)
+
+ counter.reset!
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
index 0e7d7f1efda..92ffeee8509 100644
--- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
@@ -29,8 +29,8 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
context 'when from date is given' do
before do
- Timecop.freeze(5.days.ago) { create(:issue, project: project) }
- Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
+ travel_to(5.days.ago) { create(:issue, project: project) }
+ travel_to(5.days.from_now) { create(:issue, project: project) }
end
it "finds the number of issues created after the 'from date'" do
@@ -45,15 +45,15 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
end
it "doesn't find issues from other projects" do
- Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) }
+ travel_to(5.days.from_now) { create(:issue, project: create(:project)) }
expect(subject[:value]).to eq('-')
end
context 'when `to` parameter is given' do
before do
- Timecop.freeze(5.days.ago) { create(:issue, project: project) }
- Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
+ travel_to(5.days.ago) { create(:issue, project: project) }
+ travel_to(5.days.from_now) { create(:issue, project: project) }
end
it "doesn't find any record" do
@@ -78,8 +78,8 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
context 'when from date is given' do
before do
- Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
- Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
+ travel_to(5.days.ago) { create_commit("Test message", project, user, 'master') }
+ travel_to(5.days.from_now) { create_commit("Test message", project, user, 'master') }
end
it "finds the number of commits created after the 'from date'" do
@@ -94,21 +94,21 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
end
it "doesn't find commits from other projects" do
- Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project, :repository), user, 'master') }
+ travel_to(5.days.from_now) { create_commit("Test message", create(:project, :repository), user, 'master') }
expect(subject[:value]).to eq('-')
end
it "finds a large (> 100) number of commits if present" do
- Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
+ travel_to(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
expect(subject[:value]).to eq('100')
end
context 'when `to` parameter is given' do
before do
- Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
- Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
+ travel_to(5.days.ago) { create_commit("Test message", project, user, 'master') }
+ travel_to(5.days.from_now) { create_commit("Test message", project, user, 'master') }
end
it "doesn't find any record" do
diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb
index 8ee57542d43..bf08e782035 100644
--- a/spec/lib/gitlab/data_builder/deployment_spec.rb
+++ b/spec/lib/gitlab/data_builder/deployment_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
expect(data[:object_kind]).to eq('deployment')
end
- it 'returns data for the given build' do
- environment = create(:environment, name: "somewhere")
+ it 'returns data for the given build', :aggregate_failures do
+ environment = create(:environment, name: 'somewhere/1', external_url: 'https://test.com')
project = create(:project, :repository, name: 'myproj')
commit = project.commit('HEAD')
deployment = create(:deployment, status: :failed, environment: environment, sha: commit.sha, project: project)
@@ -30,7 +30,9 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
expect(data[:deployment_id]).to eq(deployment.id)
expect(data[:deployable_id]).to eq(deployable.id)
expect(data[:deployable_url]).to eq(expected_deployable_url)
- expect(data[:environment]).to eq("somewhere")
+ expect(data[:environment]).to eq('somewhere/1')
+ expect(data[:environment_slug]).to eq('somewhere-1-78avk6')
+ expect(data[:environment_external_url]).to eq('https://test.com')
expect(data[:project]).to eq(project.hook_attrs)
expect(data[:short_sha]).to eq(deployment.short_sha)
expect(data[:user]).to eq(deployment.deployed_by.hook_attrs)
diff --git a/spec/lib/gitlab/database/gitlab_schema_spec.rb b/spec/lib/gitlab/database/gitlab_schema_spec.rb
index 72950895022..4b37cbda047 100644
--- a/spec/lib/gitlab/database/gitlab_schema_spec.rb
+++ b/spec/lib/gitlab/database/gitlab_schema_spec.rb
@@ -1,42 +1,86 @@
# frozen_string_literal: true
require 'spec_helper'
+RSpec.shared_examples 'validate path globs' do |path_globs|
+ it 'returns an array of path globs' do
+ expect(path_globs).to be_an(Array)
+ expect(path_globs).to all(be_an(Pathname))
+ end
+end
+
RSpec.describe Gitlab::Database::GitlabSchema do
- describe '.tables_to_schema' do
- it 'all tables have assigned a known gitlab_schema' do
- expect(described_class.tables_to_schema).to all(
+ describe '.views_and_tables_to_schema' do
+ it 'all tables and views have assigned a known gitlab_schema' do
+ expect(described_class.views_and_tables_to_schema).to all(
match([be_a(String), be_in(Gitlab::Database.schemas_to_base_models.keys.map(&:to_sym))])
)
end
# This being run across different databases indirectly also tests
# a general consistency of structure across databases
- Gitlab::Database.database_base_models.select { |k, _| k != 'geo' }.each do |db_config_name, db_class|
+ Gitlab::Database.database_base_models.except(:geo).each do |db_config_name, db_class|
context "for #{db_config_name} using #{db_class}" do
let(:db_data_sources) { db_class.connection.data_sources }
# The Geo database does not share the same structure as all decomposed databases
- subject { described_class.tables_to_schema.select { |_, v| v != :gitlab_geo } }
+ subject { described_class.views_and_tables_to_schema.select { |_, v| v != :gitlab_geo } }
it 'new data sources are added' do
- missing_tables = db_data_sources.to_set - subject.keys
+ missing_data_sources = db_data_sources.to_set - subject.keys
- expect(missing_tables).to be_empty, \
- "Missing table(s) #{missing_tables.to_a} not found in #{described_class}.tables_to_schema. " \
- "Any new tables must be added to #{described_class::GITLAB_SCHEMAS_FILE}."
+ expect(missing_data_sources).to be_empty, \
+ "Missing table/view(s) #{missing_data_sources.to_a} not found in " \
+ "#{described_class}.views_and_tables_to_schema. " \
+ "Any new tables or views must be added to the database dictionary. " \
+ "More info: https://docs.gitlab.com/ee/development/database/database_dictionary.html"
end
it 'non-existing data sources are removed' do
- extra_tables = subject.keys.to_set - db_data_sources
+ extra_data_sources = subject.keys.to_set - db_data_sources
- expect(extra_tables).to be_empty, \
- "Extra table(s) #{extra_tables.to_a} found in #{described_class}.tables_to_schema. " \
- "Any removed or renamed tables must be removed from #{described_class::GITLAB_SCHEMAS_FILE}."
+ expect(extra_data_sources).to be_empty, \
+ "Extra table/view(s) #{extra_data_sources.to_a} found in #{described_class}.views_and_tables_to_schema. " \
+ "Any removed or renamed tables or views must be removed from the database dictionary. " \
+ "More info: https://docs.gitlab.com/ee/development/database/database_dictionary.html"
end
end
end
end
+ describe '.dictionary_path_globs' do
+ include_examples 'validate path globs', described_class.dictionary_path_globs
+ end
+
+ describe '.view_path_globs' do
+ include_examples 'validate path globs', described_class.view_path_globs
+ end
+
+ describe '.tables_to_schema' do
+ let(:database_models) { Gitlab::Database.database_base_models.except(:geo) }
+ let(:views) { database_models.flat_map { |_, m| m.connection.views }.sort.uniq }
+
+ subject { described_class.tables_to_schema }
+
+ it 'returns only tables' do
+ tables = subject.keys
+
+ expect(tables).not_to include(views.to_set)
+ end
+ end
+
+ describe '.views_to_schema' do
+ let(:database_models) { Gitlab::Database.database_base_models.except(:geo) }
+ let(:tables) { database_models.flat_map { |_, m| m.connection.tables }.sort.uniq }
+
+ subject { described_class.views_to_schema }
+
+ it 'returns only views' do
+ views = subject.keys
+
+ expect(views).not_to include(tables.to_set)
+ end
+ end
+
describe '.table_schema' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb
index b7915e6cf69..7eb20f77417 100644
--- a/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
+RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware, feature_category: :database do
let(:middleware) { described_class.new }
let(:worker_class) { 'TestDataConsistencyWorker' }
@@ -34,8 +34,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
data_consistency data_consistency, feature_flag: feature_flag
- def perform(*args)
- end
+ def perform(*args); end
end
end
@@ -83,21 +82,41 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
allow(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary?).and_return(false)
end
- it 'passes database_replica_location' do
- expected_location = {}
+ context 'when replica hosts are available' do
+ it 'passes database_replica_location' do
+ expected_location = {}
- Gitlab::Database::LoadBalancing.each_load_balancer do |lb|
- expect(lb.host)
- .to receive(:database_replica_location)
- .and_return(location)
+ Gitlab::Database::LoadBalancing.each_load_balancer do |lb|
+ expect(lb.host)
+ .to receive(:database_replica_location)
+ .and_return(location)
- expected_location[lb.name] = location
+ expected_location[lb.name] = location
+ end
+
+ run_middleware
+
+ expect(job['wal_locations']).to eq(expected_location)
+ expect(job['wal_location_source']).to eq(:replica)
end
+ end
- run_middleware
+ context 'when no replica hosts are available' do
+ it 'passes primary_write_location' do
+ expected_location = {}
- expect(job['wal_locations']).to eq(expected_location)
- expect(job['wal_location_source']).to eq(:replica)
+ Gitlab::Database::LoadBalancing.each_load_balancer do |lb|
+ expect(lb).to receive(:host).and_return(nil)
+ expect(lb).to receive(:primary_write_location).and_return(location)
+
+ expected_location[lb.name] = location
+ end
+
+ run_middleware
+
+ expect(job['wal_locations']).to eq(expected_location)
+ expect(job['wal_location_source']).to eq(:replica)
+ end
end
include_examples 'job data consistency'
diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
index 61b63016f1a..abf10456d0a 100644
--- a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
@@ -33,8 +33,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
data_consistency data_consistency, feature_flag: feature_flag
- def perform(*args)
- end
+ def perform(*args); end
end
end
@@ -332,28 +331,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
expect(middleware.send(:databases_in_sync?, locations))
.to eq(false)
end
-
- context 'when "indifferent_wal_location_keys" FF is off' do
- before do
- stub_feature_flags(indifferent_wal_location_keys: false)
- end
-
- it 'returns true when the load balancers are not in sync' do
- locations = {}
-
- Gitlab::Database::LoadBalancing.each_load_balancer do |lb|
- locations[lb.name.to_s] = 'foo'
-
- allow(lb)
- .to receive(:select_up_to_date_host)
- .with('foo')
- .and_return(false)
- end
-
- expect(middleware.send(:databases_in_sync?, locations))
- .to eq(true)
- end
- end
end
end
diff --git a/spec/lib/gitlab/database/lock_writes_manager_spec.rb b/spec/lib/gitlab/database/lock_writes_manager_spec.rb
index b1cc8add55a..242b2040eaa 100644
--- a/spec/lib/gitlab/database/lock_writes_manager_spec.rb
+++ b/spec/lib/gitlab/database/lock_writes_manager_spec.rb
@@ -37,6 +37,14 @@ RSpec.describe Gitlab::Database::LockWritesManager do
it 'returns true for a table that is locked for writes' do
expect { subject.lock_writes }.to change { subject.table_locked_for_writes?(test_table) }.from(false).to(true)
end
+
+ context 'for detached partition tables in another schema' do
+ let(:test_table) { 'gitlab_partitions_dynamic._test_table_20220101' }
+
+ it 'returns true for a table that is locked for writes' do
+ expect { subject.lock_writes }.to change { subject.table_locked_for_writes?(test_table) }.from(false).to(true)
+ end
+ end
end
describe '#lock_writes' do
diff --git a/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb b/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb
new file mode 100644
index 00000000000..9fd49b312eb
--- /dev/null
+++ b/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb
@@ -0,0 +1,334 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
+ :reestablished_active_record_base, query_analyzers: false do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:schema_class) { Class.new(Gitlab::Database::Migration[2.1]) }
+ let(:gitlab_main_table_name) { :_test_gitlab_main_table }
+ let(:gitlab_ci_table_name) { :_test_gitlab_ci_table }
+ let(:gitlab_geo_table_name) { :_test_gitlab_geo_table }
+ let(:gitlab_shared_table_name) { :_test_table }
+
+ before do
+ stub_feature_flags(automatic_lock_writes_on_table: true)
+ reconfigure_db_connection(model: ActiveRecord::Base, config_model: config_model)
+ end
+
+ shared_examples 'does not lock writes on table' do |config_model|
+ let(:config_model) { config_model }
+
+ it 'allows deleting records from the table' do
+ allow_next_instance_of(Gitlab::Database::LockWritesManager) do |instance|
+ expect(instance).not_to receive(:lock_writes)
+ end
+
+ run_migration
+
+ expect do
+ migration_class.connection.execute("DELETE FROM #{table_name}")
+ end.not_to raise_error
+ end
+ end
+
+ shared_examples 'locks writes on table' do |config_model|
+ let(:config_model) { config_model }
+
+ it 'errors on deleting' do
+ allow_next_instance_of(Gitlab::Database::LockWritesManager) do |instance|
+ expect(instance).to receive(:lock_writes).and_call_original
+ end
+
+ run_migration
+
+ expect do
+ migration_class.connection.execute("DELETE FROM #{table_name}")
+ end.to raise_error(ActiveRecord::StatementInvalid, /is write protected/)
+ end
+ end
+
+ context 'when executing create_table migrations' do
+ let(:create_gitlab_main_table_migration_class) { create_table_migration(gitlab_main_table_name) }
+ let(:create_gitlab_ci_table_migration_class) { create_table_migration(gitlab_ci_table_name) }
+ let(:create_gitlab_shared_table_migration_class) { create_table_migration(gitlab_shared_table_name) }
+
+ context 'when single database' do
+ let(:config_model) { Gitlab::Database.database_base_models[:main] }
+
+ before do
+ skip_if_multiple_databases_are_setup
+ end
+
+ it 'does not lock any newly created tables' do
+ allow_next_instance_of(Gitlab::Database::LockWritesManager) do |instance|
+ expect(instance).not_to receive(:lock_writes)
+ end
+
+ create_gitlab_main_table_migration_class.migrate(:up)
+ create_gitlab_ci_table_migration_class.migrate(:up)
+ create_gitlab_shared_table_migration_class.migrate(:up)
+
+ expect do
+ create_gitlab_main_table_migration_class.connection.execute("DELETE FROM #{gitlab_main_table_name}")
+ create_gitlab_ci_table_migration_class.connection.execute("DELETE FROM #{gitlab_ci_table_name}")
+ create_gitlab_shared_table_migration_class.connection.execute("DELETE FROM #{gitlab_shared_table_name}")
+ end.not_to raise_error
+ end
+ end
+
+ context 'when multiple databases' do
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ let(:skip_automatic_lock_on_writes) { false }
+ let(:migration_class) { create_table_migration(table_name, skip_automatic_lock_on_writes) }
+ let(:run_migration) { migration_class.migrate(:up) }
+
+ context 'for creating a gitlab_main table' do
+ let(:table_name) { gitlab_main_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:ci]
+
+ context 'when table listed as a deleted table' do
+ before do
+ stub_const("Gitlab::Database::GitlabSchema::DELETED_TABLES", { table_name.to_s => :gitlab_main })
+ end
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'when the migration skips automatic locking of tables' do
+ let(:skip_automatic_lock_on_writes) { true }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'when the SKIP_AUTOMATIC_LOCK_ON_WRITES feature flag is set' do
+ before do
+ stub_env('SKIP_AUTOMATIC_LOCK_ON_WRITES' => 'true')
+ end
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'when the automatic_lock_writes_on_table feature flag is disabled' do
+ before do
+ stub_feature_flags(automatic_lock_writes_on_table: false)
+ end
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+ end
+
+ context 'for creating a gitlab_ci table' do
+ let(:table_name) { gitlab_ci_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:main]
+
+ context 'when table listed as a deleted table' do
+ before do
+ stub_const("Gitlab::Database::GitlabSchema::DELETED_TABLES", { table_name.to_s => :gitlab_ci })
+ end
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ end
+
+ context 'when the migration skips automatic locking of tables' do
+ let(:skip_automatic_lock_on_writes) { true }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ end
+
+ context 'when the SKIP_AUTOMATIC_LOCK_ON_WRITES feature flag is set' do
+ before do
+ stub_env('SKIP_AUTOMATIC_LOCK_ON_WRITES' => 'true')
+ end
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ end
+
+ context 'when the automatic_lock_writes_on_table feature flag is disabled' do
+ before do
+ stub_feature_flags(automatic_lock_writes_on_table: false)
+ end
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ end
+ end
+
+ context 'for creating gitlab_shared table' do
+ let(:table_name) { gitlab_shared_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'for creating a gitlab_geo table' do
+ before do
+ skip unless geo_configured?
+ end
+
+ let(:table_name) { gitlab_geo_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:geo]
+ end
+
+ context 'for creating an unknown gitlab_schema table' do
+ let(:table_name) { :foobar } # no gitlab_schema defined
+ let(:config_model) { Gitlab::Database.database_base_models[:main] }
+
+ it "raises an error about undefined gitlab_schema" do
+ expected_error_message = <<~ERROR
+ No gitlab_schema is defined for the table #{table_name}. Please consider
+ adding it to the database dictionary.
+ More info: https://docs.gitlab.com/ee/development/database/database_dictionary.html
+ ERROR
+
+ expect { run_migration }.to raise_error(expected_error_message)
+ end
+ end
+ end
+ end
+
+ context 'when renaming a table' do
+ before do
+ skip_if_multiple_databases_not_setup
+ create_table_migration(old_table_name).migrate(:up) # create the table first before renaming it
+ end
+
+ let(:migration_class) { rename_table_migration(old_table_name, table_name) }
+ let(:run_migration) { migration_class.migrate(:up) }
+
+ context 'when a gitlab_main table' do
+ let(:old_table_name) { gitlab_main_table_name }
+ let(:table_name) { :_test_gitlab_main_new_table }
+ let(:database_base_model) { Gitlab::Database.database_base_models[:main] }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'when a gitlab_ci table' do
+ let(:old_table_name) { gitlab_ci_table_name }
+ let(:table_name) { :_test_gitlab_ci_new_table }
+ let(:database_base_model) { Gitlab::Database.database_base_models[:ci] }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:main]
+ end
+ end
+
+ context 'when reversing drop_table migrations' do
+ let(:drop_gitlab_main_table_migration_class) { drop_table_migration(gitlab_main_table_name) }
+ let(:drop_gitlab_ci_table_migration_class) { drop_table_migration(gitlab_ci_table_name) }
+ let(:drop_gitlab_shared_table_migration_class) { drop_table_migration(gitlab_shared_table_name) }
+
+ context 'when single database' do
+ let(:config_model) { Gitlab::Database.database_base_models[:main] }
+
+ before do
+ skip_if_multiple_databases_are_setup
+ end
+
+ it 'does not lock any newly created tables' do
+ allow_next_instance_of(Gitlab::Database::LockWritesManager) do |instance|
+ expect(instance).not_to receive(:lock_writes)
+ end
+
+ drop_gitlab_main_table_migration_class.connection.execute("CREATE TABLE #{gitlab_main_table_name}()")
+ drop_gitlab_ci_table_migration_class.connection.execute("CREATE TABLE #{gitlab_ci_table_name}()")
+ drop_gitlab_shared_table_migration_class.connection.execute("CREATE TABLE #{gitlab_shared_table_name}()")
+
+ drop_gitlab_main_table_migration_class.migrate(:up)
+ drop_gitlab_ci_table_migration_class.migrate(:up)
+ drop_gitlab_shared_table_migration_class.migrate(:up)
+
+ drop_gitlab_main_table_migration_class.migrate(:down)
+ drop_gitlab_ci_table_migration_class.migrate(:down)
+ drop_gitlab_shared_table_migration_class.migrate(:down)
+
+ expect do
+ drop_gitlab_main_table_migration_class.connection.execute("DELETE FROM #{gitlab_main_table_name}")
+ drop_gitlab_ci_table_migration_class.connection.execute("DELETE FROM #{gitlab_ci_table_name}")
+ drop_gitlab_shared_table_migration_class.connection.execute("DELETE FROM #{gitlab_shared_table_name}")
+ end.not_to raise_error
+ end
+ end
+
+ context 'when multiple databases' do
+ before do
+ skip_if_multiple_databases_not_setup
+ migration_class.connection.execute("CREATE TABLE #{table_name}()")
+ migration_class.migrate(:up)
+ end
+
+ let(:migration_class) { drop_table_migration(table_name) }
+ let(:run_migration) { migration_class.migrate(:down) }
+
+ context 'for re-creating a gitlab_main table' do
+ let(:table_name) { gitlab_main_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'for re-creating a gitlab_ci table' do
+ let(:table_name) { gitlab_ci_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:main]
+ end
+
+ context 'for re-creating a gitlab_shared table' do
+ let(:table_name) { gitlab_shared_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+ end
+ end
+
+ def create_table_migration(table_name, skip_lock_on_writes = false)
+ migration_class = Class.new(schema_class) do
+ class << self; attr_accessor :table_name; end
+ def change
+ create_table self.class.table_name
+ end
+ end
+ migration_class.skip_automatic_lock_on_writes = skip_lock_on_writes
+ migration_class.tap { |klass| klass.table_name = table_name }
+ end
+
+ def rename_table_migration(old_table_name, new_table_name)
+ migration_class = Class.new(schema_class) do
+ class << self; attr_accessor :old_table_name, :new_table_name; end
+ def change
+ rename_table self.class.old_table_name, self.class.new_table_name
+ end
+ end
+
+ migration_class.tap do |klass|
+ klass.old_table_name = old_table_name
+ klass.new_table_name = new_table_name
+ end
+ end
+
+ def drop_table_migration(table_name)
+ migration_class = Class.new(schema_class) do
+ class << self; attr_accessor :table_name; end
+ def change
+ drop_table(self.class.table_name) {}
+ end
+ end
+ migration_class.tap { |klass| klass.table_name = table_name }
+ end
+
+ def geo_configured?
+ !!ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'geo')
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb b/spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb
index e43cfe0814e..e8045f5afec 100644
--- a/spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_a
describe '#restrict_gitlab_migration' do
it 'invalid schema raises exception' do
- expect { schema_class.restrict_gitlab_migration gitlab_schema: :gitlab_non_exisiting }
+ expect { schema_class.restrict_gitlab_migration gitlab_schema: :gitlab_non_existing }
.to raise_error /Unknown 'gitlab_schema:/
end
@@ -102,7 +102,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_a
"does add index to projects in gitlab_main and gitlab_ci" => {
migration: ->(klass) do
def change
- # Due to running in transactin we cannot use `add_concurrent_index`
+ # Due to running in transaction we cannot use `add_concurrent_index`
add_index :projects, :hidden
end
end,
@@ -185,8 +185,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_a
execute("create schema __test_schema")
end
- def down
- end
+ def down; end
end,
query_matcher: /create schema __test_schema/,
expected: {
@@ -306,8 +305,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_a
detached_partitions_class.create!(drop_after: Time.current, table_name: '_test_table')
end
- def down
- end
+ def down; end
def detached_partitions_class
Class.new(Gitlab::Database::Migration[2.0]::MigrationRecord) do
@@ -450,8 +448,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_a
ApplicationSetting.last
end
- def down
- end
+ def down; end
end,
query_matcher: /FROM "application_settings"/,
expected: {
@@ -475,8 +472,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_a
Feature.enabled?(:redis_hll_tracking, type: :ops)
end
- def down
- end
+ def down; end
end,
query_matcher: /FROM "features"/,
expected: {
@@ -505,8 +501,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_a
end
end
- def down
- end
+ def down; end
end,
query_matcher: /FROM ci_builds/,
setup: -> (_) { skip_if_multiple_databases_not_setup },
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 65fbc8d9935..30eeff31326 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1199,18 +1199,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
- describe '#add_column_with_default' do
- let(:column) { Project.columns.find { |c| c.name == "id" } }
-
- it 'delegates to #add_column' do
- expect(model).to receive(:add_column).with(:projects, :foo, :integer, default: 10, limit: nil, null: true)
-
- model.add_column_with_default(:projects, :foo, :integer,
- default: 10,
- allow_null: true)
- end
- end
-
describe '#rename_column_concurrently' do
context 'in a transaction' do
it 'raises RuntimeError' do
@@ -2006,170 +1994,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
- describe 'sidekiq migration helpers', :redis do
- let(:worker) do
- Class.new do
- include Sidekiq::Worker
-
- sidekiq_options queue: 'test'
-
- def self.name
- 'WorkerClass'
- end
- end
- end
-
- let(:same_queue_different_worker) do
- Class.new do
- include Sidekiq::Worker
-
- sidekiq_options queue: 'test'
-
- def self.name
- 'SameQueueDifferentWorkerClass'
- end
- end
- end
-
- let(:unrelated_worker) do
- Class.new do
- include Sidekiq::Worker
-
- sidekiq_options queue: 'unrelated'
-
- def self.name
- 'UnrelatedWorkerClass'
- end
- end
- end
-
- before do
- stub_const(worker.name, worker)
- stub_const(unrelated_worker.name, unrelated_worker)
- stub_const(same_queue_different_worker.name, same_queue_different_worker)
- end
-
- describe '#sidekiq_remove_jobs', :clean_gitlab_redis_queues do
- def clear_queues
- Sidekiq::Queue.new('test').clear
- Sidekiq::Queue.new('unrelated').clear
- Sidekiq::RetrySet.new.clear
- Sidekiq::ScheduledSet.new.clear
- end
-
- around do |example|
- clear_queues
- Sidekiq::Testing.disable!(&example)
- clear_queues
- end
-
- it "removes all related job instances from the job class's queue" do
- worker.perform_async
- same_queue_different_worker.perform_async
- unrelated_worker.perform_async
-
- queue_we_care_about = Sidekiq::Queue.new(worker.queue)
- unrelated_queue = Sidekiq::Queue.new(unrelated_worker.queue)
-
- expect(queue_we_care_about.size).to eq(2)
- expect(unrelated_queue.size).to eq(1)
-
- model.sidekiq_remove_jobs(job_klass: worker)
-
- expect(queue_we_care_about.size).to eq(1)
- expect(queue_we_care_about.map(&:klass)).not_to include(worker.name)
- expect(queue_we_care_about.map(&:klass)).to include(
- same_queue_different_worker.name
- )
- expect(unrelated_queue.size).to eq(1)
- end
-
- context 'when job instances are in the scheduled set' do
- it 'removes all related job instances from the scheduled set' do
- worker.perform_in(1.hour)
- unrelated_worker.perform_in(1.hour)
-
- scheduled = Sidekiq::ScheduledSet.new
-
- expect(scheduled.size).to eq(2)
- expect(scheduled.map(&:klass)).to include(
- worker.name,
- unrelated_worker.name
- )
-
- model.sidekiq_remove_jobs(job_klass: worker)
-
- expect(scheduled.size).to eq(1)
- expect(scheduled.map(&:klass)).not_to include(worker.name)
- expect(scheduled.map(&:klass)).to include(unrelated_worker.name)
- end
- end
-
- context 'when job instances are in the retry set' do
- include_context 'when handling retried jobs'
-
- it 'removes all related job instances from the retry set' do
- retry_in(worker, 1.hour)
- retry_in(worker, 2.hours)
- retry_in(worker, 3.hours)
- retry_in(unrelated_worker, 4.hours)
-
- retries = Sidekiq::RetrySet.new
-
- expect(retries.size).to eq(4)
- expect(retries.map(&:klass)).to include(
- worker.name,
- unrelated_worker.name
- )
-
- model.sidekiq_remove_jobs(job_klass: worker)
-
- expect(retries.size).to eq(1)
- expect(retries.map(&:klass)).not_to include(worker.name)
- expect(retries.map(&:klass)).to include(unrelated_worker.name)
- end
- end
- end
-
- describe '#sidekiq_queue_length' do
- context 'when queue is empty' do
- it 'returns zero' do
- Sidekiq::Testing.disable! do
- expect(model.sidekiq_queue_length('test')).to eq 0
- end
- end
- end
-
- context 'when queue contains jobs' do
- it 'returns correct size of the queue' do
- Sidekiq::Testing.disable! do
- worker.perform_async('Something', [1])
- worker.perform_async('Something', [2])
-
- expect(model.sidekiq_queue_length('test')).to eq 2
- end
- end
- end
- end
-
- describe '#sidekiq_queue_migrate' do
- it 'migrates jobs from one sidekiq queue to another' do
- Sidekiq::Testing.disable! do
- worker.perform_async('Something', [1])
- worker.perform_async('Something', [2])
-
- expect(model.sidekiq_queue_length('test')).to eq 2
- expect(model.sidekiq_queue_length('new_test')).to eq 0
-
- model.sidekiq_queue_migrate('test', to: 'new_test')
-
- expect(model.sidekiq_queue_length('test')).to eq 0
- expect(model.sidekiq_queue_length('new_test')).to eq 2
- end
- end
- end
- end
-
describe '#check_trigger_permissions!' do
it 'does nothing when the user has the correct permissions' do
expect { model.check_trigger_permissions!('users') }
@@ -2790,18 +2614,18 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
model.backfill_iids('issues')
- issue = issue_class.create!(project_id: project.id)
+ issue = issue_class.create!(project_id: project.id, namespace_id: project.project_namespace_id)
expect(issue.iid).to eq(1)
end
it 'generates iids properly for models created after the migration when iids are backfilled' do
project = setup
- issue_a = issues.create!(project_id: project.id, work_item_type_id: issue_type.id)
+ issue_a = issues.create!(project_id: project.id, namespace_id: project.project_namespace_id, work_item_type_id: issue_type.id)
model.backfill_iids('issues')
- issue_b = issue_class.create!(project_id: project.id)
+ issue_b = issue_class.create!(project_id: project.id, namespace_id: project.project_namespace_id)
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.iid).to eq(2)
@@ -2810,14 +2634,14 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
it 'generates iids properly for models created after the migration across multiple projects' do
project_a = setup
project_b = setup
- issues.create!(project_id: project_a.id, work_item_type_id: issue_type.id)
- issues.create!(project_id: project_b.id, work_item_type_id: issue_type.id)
- issues.create!(project_id: project_b.id, work_item_type_id: issue_type.id)
+ issues.create!(project_id: project_a.id, namespace_id: project_a.project_namespace_id, work_item_type_id: issue_type.id)
+ issues.create!(project_id: project_b.id, namespace_id: project_b.project_namespace_id, work_item_type_id: issue_type.id)
+ issues.create!(project_id: project_b.id, namespace_id: project_b.project_namespace_id, work_item_type_id: issue_type.id)
model.backfill_iids('issues')
- issue_a = issue_class.create!(project_id: project_a.id, work_item_type_id: issue_type.id)
- issue_b = issue_class.create!(project_id: project_b.id, work_item_type_id: issue_type.id)
+ issue_a = issue_class.create!(project_id: project_a.id, namespace_id: project_a.project_namespace_id, work_item_type_id: issue_type.id)
+ issue_b = issue_class.create!(project_id: project_b.id, namespace_id: project_b.project_namespace_id, work_item_type_id: issue_type.id)
expect(issue_a.iid).to eq(2)
expect(issue_b.iid).to eq(3)
@@ -2827,11 +2651,11 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
it 'generates an iid' do
project_a = setup
project_b = setup
- issue_a = issues.create!(project_id: project_a.id, work_item_type_id: issue_type.id)
+ issue_a = issues.create!(project_id: project_a.id, namespace_id: project_a.project_namespace_id, work_item_type_id: issue_type.id)
model.backfill_iids('issues')
- issue_b = issue_class.create!(project_id: project_b.id)
+ issue_b = issue_class.create!(project_id: project_b.id, namespace_id: project_b.project_namespace_id)
expect(issue_a.reload.iid).to eq(1)
expect(issue_b.reload.iid).to eq(1)
@@ -2841,8 +2665,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
context 'when a row already has an iid set in the database' do
it 'backfills iids' do
project = setup
- issue_a = issues.create!(project_id: project.id, work_item_type_id: issue_type.id, iid: 1)
- issue_b = issues.create!(project_id: project.id, work_item_type_id: issue_type.id, iid: 2)
+ issue_a = issues.create!(project_id: project.id, namespace_id: project.project_namespace_id, work_item_type_id: issue_type.id, iid: 1)
+ issue_b = issues.create!(project_id: project.id, namespace_id: project.project_namespace_id, work_item_type_id: issue_type.id, iid: 2)
model.backfill_iids('issues')
@@ -2853,9 +2677,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
it 'backfills for multiple projects' do
project_a = setup
project_b = setup
- issue_a = issues.create!(project_id: project_a.id, work_item_type_id: issue_type.id, iid: 1)
- issue_b = issues.create!(project_id: project_b.id, work_item_type_id: issue_type.id, iid: 1)
- issue_c = issues.create!(project_id: project_a.id, work_item_type_id: issue_type.id, iid: 2)
+ issue_a = issues.create!(project_id: project_a.id, namespace_id: project_a.project_namespace_id, work_item_type_id: issue_type.id, iid: 1)
+ issue_b = issues.create!(project_id: project_b.id, namespace_id: project_b.project_namespace_id, work_item_type_id: issue_type.id, iid: 1)
+ issue_c = issues.create!(project_id: project_a.id, namespace_id: project_a.project_namespace_id, work_item_type_id: issue_type.id, iid: 2)
model.backfill_iids('issues')
diff --git a/spec/lib/gitlab/database/migrations/batched_migration_last_id_spec.rb b/spec/lib/gitlab/database/migrations/batched_migration_last_id_spec.rb
new file mode 100644
index 00000000000..97b432406eb
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/batched_migration_last_id_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Migrations::BatchedMigrationLastId, feature_category: :pipeline_insights do
+ subject(:test_sampling) { described_class.new(connection, base_dir) }
+
+ let(:base_dir) { Pathname.new(Dir.mktmpdir) }
+ let(:file_name) { 'last-batched-background-migration-id.txt' }
+ let(:file_path) { base_dir.join(file_name) }
+ let(:file_contents) { nil }
+
+ where(:base_model) do
+ [
+ [ApplicationRecord], [Ci::ApplicationRecord]
+ ]
+ end
+
+ with_them do
+ let(:connection) { base_model.connection }
+
+ after do
+ FileUtils.rm_rf(file_path)
+ end
+
+ describe '#read' do
+ before do
+ File.write(file_path, file_contents)
+ end
+
+ context 'when the file exists and have content' do
+ let(:file_contents) { 99 }
+
+ it { expect(test_sampling.read).to eq(file_contents) }
+ end
+
+ context 'when the file exists and is blank' do
+ it { expect(test_sampling.read).to be_nil }
+ end
+
+ context "when the file doesn't exists" do
+ before do
+ FileUtils.rm_rf(file_path)
+ end
+
+ it { expect(test_sampling.read).to be_nil }
+ end
+ end
+
+ describe '#store' do
+ let(:file_contents) { File.read(file_path) }
+ let(:migration) do
+ Gitlab::Database::SharedModel.using_connection(connection) do
+ create(:batched_background_migration)
+ end
+ end
+
+ it 'creates the file properly' do
+ test_sampling.store
+
+ expect(File).to exist(file_path)
+ end
+
+ it 'stores the proper id in the file' do
+ migration
+ test_sampling.store
+
+ expect(file_contents).to eq(migration.id.to_s)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migrations/runner_spec.rb b/spec/lib/gitlab/database/migrations/runner_spec.rb
index bd382547689..66eb5a5de51 100644
--- a/spec/lib/gitlab/database/migrations/runner_spec.rb
+++ b/spec/lib/gitlab/database/migrations/runner_spec.rb
@@ -230,5 +230,13 @@ RSpec.describe Gitlab::Database::Migrations::Runner, :reestablished_active_recor
end
end
end
+
+ describe '.batched_migrations_last_id' do
+ let(:runner_class) { Gitlab::Database::Migrations::BatchedMigrationLastId }
+
+ it 'matches the expected runner class' do
+ expect(described_class.batched_migrations_last_id(database)).to be_a(runner_class)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/migrations/sidekiq_helpers_spec.rb b/spec/lib/gitlab/database/migrations/sidekiq_helpers_spec.rb
new file mode 100644
index 00000000000..fb1cb46171f
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/sidekiq_helpers_spec.rb
@@ -0,0 +1,276 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Gitlab::Database::Migrations::SidekiqHelpers do
+ let(:model) do
+ ActiveRecord::Migration.new.extend(described_class)
+ end
+
+ describe "sidekiq migration helpers", :redis do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+
+ sidekiq_options queue: "test"
+
+ def self.name
+ "WorkerClass"
+ end
+ end
+ end
+
+ let(:worker_two) do
+ Class.new do
+ include Sidekiq::Worker
+
+ sidekiq_options queue: "test_two"
+
+ def self.name
+ "WorkerTwoClass"
+ end
+ end
+ end
+
+ let(:same_queue_different_worker) do
+ Class.new do
+ include Sidekiq::Worker
+
+ sidekiq_options queue: "test"
+
+ def self.name
+ "SameQueueDifferentWorkerClass"
+ end
+ end
+ end
+
+ let(:unrelated_worker) do
+ Class.new do
+ include Sidekiq::Worker
+
+ sidekiq_options queue: "unrelated"
+
+ def self.name
+ "UnrelatedWorkerClass"
+ end
+ end
+ end
+
+ before do
+ stub_const(worker.name, worker)
+ stub_const(worker_two.name, worker_two)
+ stub_const(unrelated_worker.name, unrelated_worker)
+ stub_const(same_queue_different_worker.name, same_queue_different_worker)
+ end
+
+ describe "#sidekiq_remove_jobs", :clean_gitlab_redis_queues do
+ def clear_queues
+ Sidekiq::Queue.new("test").clear
+ Sidekiq::Queue.new("test_two").clear
+ Sidekiq::Queue.new("unrelated").clear
+ Sidekiq::RetrySet.new.clear
+ Sidekiq::ScheduledSet.new.clear
+ end
+
+ around do |example|
+ clear_queues
+ Sidekiq::Testing.disable!(&example)
+ clear_queues
+ end
+
+ context "when the constant is not defined" do
+ it "doesn't try to delete it" do
+ my_non_constant = +"SomeThingThatIsNotAConstant"
+
+ expect(Sidekiq::Queue).not_to receive(:new).with(any_args)
+ model.sidekiq_remove_jobs(job_klasses: [my_non_constant])
+ end
+ end
+
+ context "when the constant is defined" do
+ it "will use it find job instances to delete" do
+ my_constant = worker.name
+ expect(Sidekiq::Queue)
+ .to receive(:new)
+ .with(worker.queue)
+ .and_call_original
+ model.sidekiq_remove_jobs(job_klasses: [my_constant])
+ end
+ end
+
+ it "removes all related job instances from the job classes' queues" do
+ worker.perform_async
+ worker_two.perform_async
+ same_queue_different_worker.perform_async
+ unrelated_worker.perform_async
+
+ worker_queue = Sidekiq::Queue.new(worker.queue)
+ worker_two_queue = Sidekiq::Queue.new(worker_two.queue)
+ unrelated_queue = Sidekiq::Queue.new(unrelated_worker.queue)
+
+ expect(worker_queue.size).to eq(2)
+ expect(worker_two_queue.size).to eq(1)
+ expect(unrelated_queue.size).to eq(1)
+
+ model.sidekiq_remove_jobs(job_klasses: [worker.name, worker_two.name])
+
+ expect(worker_queue.size).to eq(1)
+ expect(worker_two_queue.size).to eq(0)
+ expect(worker_queue.map(&:klass)).not_to include(worker.name)
+ expect(worker_queue.map(&:klass)).to include(
+ same_queue_different_worker.name
+ )
+ expect(worker_two_queue.map(&:klass)).not_to include(worker_two.name)
+ expect(unrelated_queue.size).to eq(1)
+ end
+
+ context "when job instances are in the scheduled set" do
+ it "removes all related job instances from the scheduled set" do
+ worker.perform_in(1.hour)
+ worker_two.perform_in(1.hour)
+ unrelated_worker.perform_in(1.hour)
+
+ scheduled = Sidekiq::ScheduledSet.new
+
+ expect(scheduled.size).to eq(3)
+ expect(scheduled.map(&:klass)).to include(
+ worker.name,
+ worker_two.name,
+ unrelated_worker.name
+ )
+
+ model.sidekiq_remove_jobs(job_klasses: [worker.name, worker_two.name])
+
+ expect(scheduled.size).to eq(1)
+ expect(scheduled.map(&:klass)).not_to include(worker.name)
+ expect(scheduled.map(&:klass)).not_to include(worker_two.name)
+ expect(scheduled.map(&:klass)).to include(unrelated_worker.name)
+ end
+ end
+
+ context "when job instances are in the retry set" do
+ include_context "when handling retried jobs"
+
+ it "removes all related job instances from the retry set" do
+ retry_in(worker, 1.hour)
+ retry_in(worker, 2.hours)
+ retry_in(worker, 3.hours)
+ retry_in(worker_two, 4.hours)
+ retry_in(unrelated_worker, 5.hours)
+
+ retries = Sidekiq::RetrySet.new
+
+ expect(retries.size).to eq(5)
+ expect(retries.map(&:klass)).to include(
+ worker.name,
+ worker_two.name,
+ unrelated_worker.name
+ )
+
+ model.sidekiq_remove_jobs(job_klasses: [worker.name, worker_two.name])
+
+ expect(retries.size).to eq(1)
+ expect(retries.map(&:klass)).not_to include(worker.name)
+ expect(retries.map(&:klass)).not_to include(worker_two.name)
+ expect(retries.map(&:klass)).to include(unrelated_worker.name)
+ end
+ end
+
+ # Imitate job deletion returning zero and then non zero.
+ context "when job fails to be deleted" do
+ let(:job_double) do
+ instance_double(
+ "Sidekiq::JobRecord",
+ klass: worker.name
+ )
+ end
+
+ context "and does not work enough times in a row before max attempts" do
+ it "tries the max attempts without succeeding" do
+ worker.perform_async
+
+ allow(job_double).to receive(:delete).and_return(true)
+
+ # Scheduled set runs last so only need to stub out its values.
+ allow(Sidekiq::ScheduledSet)
+ .to receive(:new)
+ .and_return([job_double])
+
+ expect(model.sidekiq_remove_jobs(job_klasses: [worker.name]))
+ .to eq(
+ {
+ attempts: 5,
+ success: false
+ }
+ )
+ end
+ end
+
+ context "and then it works enough times in a row before max attempts" do
+ it "succeeds" do
+ worker.perform_async
+
+ # attempt 1: false will increment the streak once to 1
+ # attempt 2: true resets it back to 0
+ # attempt 3: false will increment the streak once to 1
+ # attempt 4: false will increment the streak once to 2, loop breaks
+ allow(job_double).to receive(:delete).and_return(false, true, false)
+
+ worker.perform_async
+
+ # Scheduled set runs last so only need to stub out its values.
+ allow(Sidekiq::ScheduledSet)
+ .to receive(:new)
+ .and_return([job_double])
+
+ expect(model.sidekiq_remove_jobs(job_klasses: [worker.name]))
+ .to eq(
+ {
+ attempts: 4,
+ success: true
+ }
+ )
+ end
+ end
+ end
+ end
+
+ describe "#sidekiq_queue_length" do
+ context "when queue is empty" do
+ it "returns zero" do
+ Sidekiq::Testing.disable! do
+ expect(model.sidekiq_queue_length("test")).to eq 0
+ end
+ end
+ end
+
+ context "when queue contains jobs" do
+ it "returns correct size of the queue" do
+ Sidekiq::Testing.disable! do
+ worker.perform_async("Something", [1])
+ worker.perform_async("Something", [2])
+
+ expect(model.sidekiq_queue_length("test")).to eq 2
+ end
+ end
+ end
+ end
+
+ describe "#sidekiq_queue_migrate" do
+ it "migrates jobs from one sidekiq queue to another" do
+ Sidekiq::Testing.disable! do
+ worker.perform_async("Something", [1])
+ worker.perform_async("Something", [2])
+
+ expect(model.sidekiq_queue_length("test")).to eq 2
+ expect(model.sidekiq_queue_length("new_test")).to eq 0
+
+ model.sidekiq_queue_migrate("test", to: "new_test")
+
+ expect(model.sidekiq_queue_length("test")).to eq 0
+ expect(model.sidekiq_queue_length("new_test")).to eq 2
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb
index 07226f3d025..73d69d55e5a 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
@@ -55,6 +55,8 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
let(:table_name) { "_test_column_copying" }
+ let(:from_id) { 0 }
+
before do
connection.execute(<<~SQL)
CREATE TABLE #{table_name} (
@@ -76,7 +78,8 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
end
subject(:sample_migration) do
- described_class.new(result_dir: result_dir, connection: connection).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
@@ -111,20 +114,26 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
job_interval: 5.minutes,
batch_size: 100)
- described_class.new(result_dir: result_dir, connection: connection).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
context 'with multiple jobs to run' do
- it 'runs all jobs created within the last 3 hours' do
+ let(:last_id) do
+ Gitlab::Database::SharedModel.using_connection(connection) do
+ Gitlab::Database::BackgroundMigration::BatchedMigration.maximum(:id)
+ end
+ end
+
+ 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)
- travel 4.hours
-
+ last_id
new_migration = define_background_migration('NewMigration') { travel 1.second }
queue_migration('NewMigration', table_name, :id,
job_interval: 5.minutes,
@@ -138,14 +147,15 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
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).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
end
context 'choosing uniform batches to run' do
- subject { described_class.new(result_dir: result_dir, connection: connection) }
+ subject { described_class.new(result_dir: result_dir, connection: connection, from_id: from_id) }
describe '#uniform_fractions' do
it 'generates evenly distributed sequences of fractions' do
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 336dec3a8a0..646ae50fb44 100644
--- a/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
SQL
end
- def create_partition(name:, table: 'parent_table', from:, to:, attached:, drop_after:)
+ def create_partition(name:, from:, to:, attached:, drop_after:, table: 'parent_table')
from = from.beginning_of_month
to = to.beginning_of_month
full_name = "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{name}"
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 550f254c4da..e6014f81b74 100644
--- a/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb
@@ -229,11 +229,9 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy do
next_partition_if: method(:next_partition_if_wrapper),
detach_partition_if: method(:detach_partition_if_wrapper)
- def self.next_partition?(current_partition)
- end
+ def self.next_partition?(current_partition); end
- def self.detach_partition?(partition)
- end
+ def self.detach_partition?(partition); end
end
end
diff --git a/spec/lib/gitlab/database/postgresql_adapter/dump_schema_versions_mixin_spec.rb b/spec/lib/gitlab/database/postgresql_adapter/dump_schema_versions_mixin_spec.rb
index 8b06f068503..884c4f625dd 100644
--- a/spec/lib/gitlab/database/postgresql_adapter/dump_schema_versions_mixin_spec.rb
+++ b/spec/lib/gitlab/database/postgresql_adapter/dump_schema_versions_mixin_spec.rb
@@ -9,8 +9,7 @@ RSpec.describe Gitlab::Database::PostgresqlAdapter::DumpSchemaVersionsMixin do
original_dump_schema_information
end
- def original_dump_schema_information
- end
+ def original_dump_schema_information; end
end
klass.prepend(described_class)
diff --git a/spec/lib/gitlab/database/postgresql_database_tasks/load_schema_versions_mixin_spec.rb b/spec/lib/gitlab/database/postgresql_database_tasks/load_schema_versions_mixin_spec.rb
index 3e675a85999..3bb206c6627 100644
--- a/spec/lib/gitlab/database/postgresql_database_tasks/load_schema_versions_mixin_spec.rb
+++ b/spec/lib/gitlab/database/postgresql_database_tasks/load_schema_versions_mixin_spec.rb
@@ -9,8 +9,7 @@ RSpec.describe Gitlab::Database::PostgresqlDatabaseTasks::LoadSchemaVersionsMixi
original_structure_load
end
- def original_structure_load
- end
+ def original_structure_load; end
end
klass.prepend(described_class)
diff --git a/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb b/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb
index ec01ae623ae..bcc39c0c3db 100644
--- a/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb
@@ -10,29 +10,76 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::QueryRecorder, query_analyzers:
end
end
- context 'when analyzer is enabled for tests' do
+ context 'with query analyzer' do
let(:query) { 'SELECT 1 FROM projects' }
- let(:log_path) { Rails.root.join(described_class::LOG_FILE) }
+ let(:log_path) { Rails.root.join(described_class::LOG_PATH) }
+ let(:log_file) { described_class.log_file }
- before do
- stub_env('CI', 'true')
+ after do
+ ::Gitlab::Database::QueryAnalyzer.instance.end!([described_class])
+ end
- # This is needed to be able to stub_env the CI variable
- ::Gitlab::Database::QueryAnalyzer.instance.begin!([described_class])
+ shared_examples_for 'an enabled query recorder' do
+ it 'logs queries to a file' do
+ allow(FileUtils).to receive(:mkdir_p)
+ .with(log_path)
+ expect(File).to receive(:write)
+ .with(log_file, /^{"sql":"#{query}/, mode: 'a')
+ expect(described_class).to receive(:analyze).with(/^#{query}/).and_call_original
+
+ expect { ApplicationRecord.connection.execute(query) }.not_to raise_error
+ end
end
- after do
- ::Gitlab::Database::QueryAnalyzer.instance.end!([described_class])
+ context 'on default branch' do
+ before do
+ stub_env('CI_MERGE_REQUEST_LABELS', nil)
+ stub_env('CI_DEFAULT_BRANCH', 'default_branch_name')
+ stub_env('CI_COMMIT_REF_NAME', 'default_branch_name')
+
+ # This is needed to be able to stub_env the CI variable
+ ::Gitlab::Database::QueryAnalyzer.instance.begin!([described_class])
+ end
+
+ it_behaves_like 'an enabled query recorder'
+ end
+
+ context 'on database merge requests' do
+ before do
+ stub_env('CI_MERGE_REQUEST_LABELS', 'database')
+
+ # This is needed to be able to stub_env the CI variable
+ ::Gitlab::Database::QueryAnalyzer.instance.begin!([described_class])
+ end
+
+ it_behaves_like 'an enabled query recorder'
+ end
+ end
+
+ describe '.log_file' do
+ let(:folder) { 'query_recorder' }
+ let(:extension) { 'ndjson' }
+ let(:default_name) { 'rspec' }
+ let(:job_name) { 'test-job-1' }
+
+ subject { described_class.log_file.to_s }
+
+ context 'when in CI' do
+ before do
+ stub_env('CI_JOB_NAME_SLUG', job_name)
+ end
+
+ it { is_expected.to include("#{folder}/#{job_name}.#{extension}") }
+ it { is_expected.not_to include("#{folder}/#{default_name}.#{extension}") }
end
- it 'logs queries to a file' do
- allow(FileUtils).to receive(:mkdir_p)
- .with(File.dirname(log_path))
- expect(File).to receive(:write)
- .with(log_path, /^{"sql":"#{query}/, mode: 'a')
- expect(described_class).to receive(:analyze).with(/^#{query}/).and_call_original
+ context 'when not in CI' do
+ before do
+ stub_env('CI_JOB_NAME_SLUG', nil)
+ end
- expect { ApplicationRecord.connection.execute(query) }.not_to raise_error
+ it { is_expected.to include("#{folder}/#{default_name}.#{extension}") }
+ it { is_expected.not_to include("#{folder}/#{job_name}.#{extension}") }
end
end
end
diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb
index 4c98185e780..fa26aa59329 100644
--- a/spec/lib/gitlab/database/reindexing_spec.rb
+++ b/spec/lib/gitlab/database/reindexing_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::Reindexing do
+RSpec.describe Gitlab::Database::Reindexing, feature_category: :database do
include ExclusiveLeaseHelpers
include Database::DatabaseHelpers
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
index ac2de43b7c6..c507bce634e 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
@@ -97,39 +97,48 @@ RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespa
let(:namespace) { create(:group, name: 'hello-group') }
it 'moves a project for a namespace' do
- create(:project, :repository, :legacy_storage, namespace: namespace, path: 'hello-project')
- expected_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- File.join(TestEnv.repos_path, 'bye-group', 'hello-project.git')
- end
+ project = create(:project, :repository, :legacy_storage, namespace: namespace, path: 'hello-project')
+ expected_repository = Gitlab::Git::Repository.new(
+ project.repository_storage,
+ 'bye-group/hello-project.git',
+ nil,
+ nil
+ )
subject.move_repositories(namespace, 'hello-group', 'bye-group')
- expect(File.directory?(expected_path)).to be(true)
+ expect(expected_repository).to exist
end
it 'moves a namespace in a subdirectory correctly' do
child_namespace = create(:group, name: 'sub-group', parent: namespace)
- create(:project, :repository, :legacy_storage, namespace: child_namespace, path: 'hello-project')
+ project = create(:project, :repository, :legacy_storage, namespace: child_namespace, path: 'hello-project')
- expected_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- File.join(TestEnv.repos_path, 'hello-group', 'renamed-sub-group', 'hello-project.git')
- end
+ expected_repository = Gitlab::Git::Repository.new(
+ project.repository_storage,
+ 'hello-group/renamed-sub-group/hello-project.git',
+ nil,
+ nil
+ )
subject.move_repositories(child_namespace, 'hello-group/sub-group', 'hello-group/renamed-sub-group')
- expect(File.directory?(expected_path)).to be(true)
+ expect(expected_repository).to exist
end
it 'moves a parent namespace with subdirectories' do
child_namespace = create(:group, name: 'sub-group', parent: namespace)
- create(:project, :repository, :legacy_storage, namespace: child_namespace, path: 'hello-project')
- expected_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- File.join(TestEnv.repos_path, 'renamed-group', 'sub-group', 'hello-project.git')
- end
+ project = create(:project, :repository, :legacy_storage, namespace: child_namespace, path: 'hello-project')
+ expected_repository = Gitlab::Git::Repository.new(
+ project.repository_storage,
+ 'renamed-group/sub-group/hello-project.git',
+ nil,
+ nil
+ )
subject.move_repositories(child_namespace, 'hello-group', 'renamed-group')
- expect(File.directory?(expected_path)).to be(true)
+ expect(expected_repository).to exist
end
end
@@ -175,14 +184,17 @@ RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespa
describe '#rename_namespace_dependencies' do
it "moves the repository for a project in the namespace" do
- create(:project, :repository, :legacy_storage, namespace: namespace, path: "the-path-project")
- expected_repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- File.join(TestEnv.repos_path, "the-path0", "the-path-project.git")
- end
+ project = create(:project, :repository, :legacy_storage, namespace: namespace, path: "the-path-project")
+ expected_repository = Gitlab::Git::Repository.new(
+ project.repository_storage,
+ "the-path0/the-path-project.git",
+ nil,
+ nil
+ )
subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0')
- expect(File.directory?(expected_repo)).to be(true)
+ expect(expected_repository).to exist
end
it "moves the uploads for the namespace" do
@@ -276,9 +288,7 @@ RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespa
project.create_repository
subject.rename_namespace(namespace)
- expected_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- File.join(TestEnv.repos_path, 'the-path', 'a-project.git')
- end
+ expected_repository = Gitlab::Git::Repository.new(project.repository_storage, 'the-path/a-project.git', nil, nil)
expect(subject).to receive(:rename_namespace_dependencies)
.with(
@@ -289,7 +299,7 @@ RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespa
subject.revert_renames
- expect(File.directory?(expected_path)).to be_truthy
+ expect(expected_repository).to exist
end
it "doesn't break when the namespace was renamed" do
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
index 6292f0246f7..aa2a3329477 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
@@ -126,13 +126,16 @@ RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProject
let(:project) { create(:project, :repository, :legacy_storage, path: 'the-path', namespace: known_parent) }
it 'moves the repository for a project' do
- expected_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- File.join(TestEnv.repos_path, 'known-parent', 'new-repo.git')
- end
+ expected_repository = Gitlab::Git::Repository.new(
+ project.repository_storage,
+ 'known-parent/new-repo.git',
+ nil,
+ nil
+ )
subject.move_repository(project, 'known-parent/the-path', 'known-parent/new-repo')
- expect(File.directory?(expected_path)).to be(true)
+ expect(expected_repository).to exist
end
end
@@ -157,9 +160,12 @@ RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProject
project.create_repository
subject.rename_project(project)
- expected_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- File.join(TestEnv.repos_path, 'known-parent', 'the-path.git')
- end
+ expected_repository = Gitlab::Git::Repository.new(
+ project.repository_storage,
+ 'known-parent/the-path.git',
+ nil,
+ nil
+ )
expect(subject).to receive(:move_project_folders)
.with(
@@ -170,7 +176,7 @@ RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProject
subject.revert_renames
- expect(File.directory?(expected_path)).to be_truthy
+ expect(expected_repository).to exist
end
it "doesn't break when the project was renamed" do
diff --git a/spec/lib/gitlab/database/schema_cleaner_spec.rb b/spec/lib/gitlab/database/schema_cleaner_spec.rb
index 950759c7f96..5283b34ca86 100644
--- a/spec/lib/gitlab/database/schema_cleaner_spec.rb
+++ b/spec/lib/gitlab/database/schema_cleaner_spec.rb
@@ -19,6 +19,15 @@ RSpec.describe Gitlab::Database::SchemaCleaner do
expect(subject).not_to match(/public\.\w+/)
end
+ it 'cleans up all the gitlab_schema_prevent_write table triggers' do
+ expect(subject).not_to match(/CREATE TRIGGER gitlab_schema_write_trigger_for_\w+/)
+ expect(subject).not_to match(/FOR EACH STATEMENT EXECUTE FUNCTION gitlab_schema_prevent_write/)
+ end
+
+ it 'keeps the lock_writes trigger functions' do
+ expect(subject).to match(/CREATE FUNCTION gitlab_schema_prevent_write/)
+ end
+
it 'cleans up the full schema as expected (blackbox test with example)' do
expected_schema = fixture_file(File.join('gitlab', 'database', 'structure_example_cleaned.sql'))
diff --git a/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb b/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb
index 97abd6d23bd..aa25590ed58 100644
--- a/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb
+++ b/spec/lib/gitlab/database/tables_sorted_by_foreign_keys_spec.rb
@@ -4,7 +4,10 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::TablesSortedByForeignKeys do
let(:connection) { ApplicationRecord.connection }
- let(:tables) { %w[_test_gitlab_main_items _test_gitlab_main_references] }
+ let(:tables) do
+ %w[_test_gitlab_main_items _test_gitlab_main_references _test_gitlab_partition_parent
+ gitlab_partitions_dynamic._test_gitlab_partition_20220101]
+ end
subject do
described_class.new(connection, tables).execute
@@ -19,13 +22,33 @@ RSpec.describe Gitlab::Database::TablesSortedByForeignKeys do
item_id BIGINT NOT NULL,
CONSTRAINT fk_constrained_1 FOREIGN KEY(item_id) REFERENCES _test_gitlab_main_items(id)
);
+
+ CREATE TABLE _test_gitlab_partition_parent (
+ id bigserial not null,
+ created_at timestamptz not null,
+ item_id BIGINT NOT NULL,
+ primary key (id, created_at),
+ CONSTRAINT fk_constrained_1 FOREIGN KEY(item_id) REFERENCES _test_gitlab_main_items(id)
+ ) PARTITION BY RANGE(created_at);
+
+ CREATE TABLE gitlab_partitions_dynamic._test_gitlab_partition_20220101
+ PARTITION OF _test_gitlab_partition_parent
+ FOR VALUES FROM ('20220101') TO ('20220131');
+
+ ALTER TABLE _test_gitlab_partition_parent DETACH PARTITION gitlab_partitions_dynamic._test_gitlab_partition_20220101;
SQL
connection.execute(statement)
end
describe '#execute' do
it 'returns the tables sorted by the foreign keys dependency' do
- expect(subject).to eq([['_test_gitlab_main_references'], ['_test_gitlab_main_items']])
+ expect(subject).to eq(
+ [
+ ['_test_gitlab_main_references'],
+ ['_test_gitlab_partition_parent'],
+ ['gitlab_partitions_dynamic._test_gitlab_partition_20220101'],
+ ['_test_gitlab_main_items']
+ ])
end
it 'returns both tables together if they are strongly connected' do
@@ -35,7 +58,12 @@ RSpec.describe Gitlab::Database::TablesSortedByForeignKeys do
SQL
connection.execute(statement)
- expect(subject).to eq([tables])
+ expect(subject).to eq(
+ [
+ ['_test_gitlab_partition_parent'],
+ ['gitlab_partitions_dynamic._test_gitlab_partition_20220101'],
+ %w[_test_gitlab_main_items _test_gitlab_main_references]
+ ])
end
end
end
diff --git a/spec/lib/gitlab/database/tables_truncate_spec.rb b/spec/lib/gitlab/database/tables_truncate_spec.rb
index 4f68cd93a8e..4d04bd67a1e 100644
--- a/spec/lib/gitlab/database/tables_truncate_spec.rb
+++ b/spec/lib/gitlab/database/tables_truncate_spec.rb
@@ -6,14 +6,9 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
:suppress_gitlab_schemas_validate_connection do
include MigrationsHelpers
- let(:logger) { instance_double(Logger) }
- let(:dry_run) { false }
- let(:until_table) { nil }
let(:min_batch_size) { 1 }
let(:main_connection) { ApplicationRecord.connection }
let(:ci_connection) { Ci::ApplicationRecord.connection }
- let(:test_gitlab_main_table) { '_test_gitlab_main_table' }
- let(:test_gitlab_ci_table) { '_test_gitlab_ci_table' }
# Main Database
let(:main_db_main_item_model) { table("_test_gitlab_main_items", database: "main") }
@@ -21,24 +16,37 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
let(:main_db_ci_item_model) { table("_test_gitlab_ci_items", database: "main") }
let(:main_db_ci_reference_model) { table("_test_gitlab_ci_references", database: "main") }
let(:main_db_shared_item_model) { table("_test_gitlab_shared_items", database: "main") }
+ let(:main_db_partitioned_item) { table("_test_gitlab_hook_logs", database: "main") }
+ let(:main_db_partitioned_item_detached) do
+ table("gitlab_partitions_dynamic._test_gitlab_hook_logs_20220101", database: "main")
+ end
+
# CI Database
let(:ci_db_main_item_model) { table("_test_gitlab_main_items", database: "ci") }
let(:ci_db_main_reference_model) { table("_test_gitlab_main_references", database: "ci") }
let(:ci_db_ci_item_model) { table("_test_gitlab_ci_items", database: "ci") }
let(:ci_db_ci_reference_model) { table("_test_gitlab_ci_references", database: "ci") }
let(:ci_db_shared_item_model) { table("_test_gitlab_shared_items", database: "ci") }
-
- subject(:truncate_legacy_tables) do
- described_class.new(
- database_name: database_name,
- min_batch_size: min_batch_size,
- logger: logger,
- dry_run: dry_run,
- until_table: until_table
- ).execute
+ let(:ci_db_partitioned_item) { table("_test_gitlab_hook_logs", database: "ci") }
+ let(:ci_db_partitioned_item_detached) do
+ table("gitlab_partitions_dynamic._test_gitlab_hook_logs_20220101", database: "ci")
end
shared_examples 'truncating legacy tables on a database' do
+ let(:logger) { instance_double(Logger) }
+ let(:dry_run) { false }
+ let(:until_table) { nil }
+
+ subject(:truncate_legacy_tables) do
+ described_class.new(
+ database_name: connection.pool.db_config.name,
+ min_batch_size: min_batch_size,
+ logger: logger,
+ dry_run: dry_run,
+ until_table: until_table
+ ).execute
+ end
+
before do
skip_if_multiple_databases_not_setup
@@ -51,6 +59,24 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
item_id BIGINT NOT NULL,
CONSTRAINT fk_constrained_1 FOREIGN KEY(item_id) REFERENCES _test_gitlab_main_items(id)
);
+
+ CREATE TABLE _test_gitlab_hook_logs (
+ id bigserial not null,
+ created_at timestamptz not null,
+ item_id BIGINT NOT NULL,
+ primary key (id, created_at),
+ CONSTRAINT fk_constrained_1 FOREIGN KEY(item_id) REFERENCES _test_gitlab_main_items(id)
+ ) PARTITION BY RANGE(created_at);
+
+ CREATE TABLE gitlab_partitions_dynamic._test_gitlab_hook_logs_20220101
+ PARTITION OF _test_gitlab_hook_logs
+ FOR VALUES FROM ('20220101') TO ('20220131');
+
+ CREATE TABLE gitlab_partitions_dynamic._test_gitlab_hook_logs_20220201
+ PARTITION OF _test_gitlab_hook_logs
+ FOR VALUES FROM ('20220201') TO ('20220228');
+
+ ALTER TABLE _test_gitlab_hook_logs DETACH PARTITION gitlab_partitions_dynamic._test_gitlab_hook_logs_20220101;
SQL
main_connection.execute(main_tables_sql)
@@ -84,18 +110,49 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
main_db_ci_item_model.create!(id: i)
main_db_ci_reference_model.create!(item_id: i)
main_db_shared_item_model.create!(id: i)
+ main_db_partitioned_item.create!(item_id: i, created_at: '2022-02-02 02:00')
+ main_db_partitioned_item_detached.create!(item_id: i, created_at: '2022-01-01 01:00')
# CI Database
ci_db_main_item_model.create!(id: i)
ci_db_main_reference_model.create!(item_id: i)
ci_db_ci_item_model.create!(id: i)
ci_db_ci_reference_model.create!(item_id: i)
ci_db_shared_item_model.create!(id: i)
+ ci_db_partitioned_item.create!(item_id: i, created_at: '2022-02-02 02:00')
+ ci_db_partitioned_item_detached.create!(item_id: i, created_at: '2022-01-01 01:00')
+ end
+
+ Gitlab::Database::SharedModel.using_connection(main_connection) do
+ Postgresql::DetachedPartition.create!(
+ table_name: '_test_gitlab_hook_logs_20220101',
+ drop_after: Time.current
+ )
+ end
+
+ Gitlab::Database::SharedModel.using_connection(ci_connection) do
+ Postgresql::DetachedPartition.create!(
+ table_name: '_test_gitlab_hook_logs_20220101',
+ drop_after: Time.current
+ )
end
allow(Gitlab::Database::GitlabSchema).to receive(:tables_to_schema).and_return(
{
"_test_gitlab_main_items" => :gitlab_main,
"_test_gitlab_main_references" => :gitlab_main,
+ "_test_gitlab_hook_logs" => :gitlab_main,
+ "_test_gitlab_ci_items" => :gitlab_ci,
+ "_test_gitlab_ci_references" => :gitlab_ci,
+ "_test_gitlab_shared_items" => :gitlab_shared,
+ "_test_gitlab_geo_items" => :gitlab_geo
+ }
+ )
+
+ allow(Gitlab::Database::GitlabSchema).to receive(:views_and_tables_to_schema).and_return(
+ {
+ "_test_gitlab_main_items" => :gitlab_main,
+ "_test_gitlab_main_references" => :gitlab_main,
+ "_test_gitlab_hook_logs" => :gitlab_main,
"_test_gitlab_ci_items" => :gitlab_ci,
"_test_gitlab_ci_references" => :gitlab_ci,
"_test_gitlab_shared_items" => :gitlab_shared,
@@ -119,7 +176,7 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
Gitlab::Database::LockWritesManager.new(
table_name: table,
connection: connection,
- database_name: database_name
+ database_name: connection.pool.db_config.name
).lock_writes
end
end
@@ -199,7 +256,6 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
context 'when truncating gitlab_ci tables on the main database' do
let(:connection) { ApplicationRecord.connection }
- let(:database_name) { "main" }
let(:legacy_tables_models) { [main_db_ci_item_model, main_db_ci_reference_model] }
let(:referencing_table_model) { main_db_ci_reference_model }
let(:referenced_table_model) { main_db_ci_item_model }
@@ -217,8 +273,10 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
context 'when truncating gitlab_main tables on the ci database' do
let(:connection) { Ci::ApplicationRecord.connection }
- let(:database_name) { "ci" }
- let(:legacy_tables_models) { [ci_db_main_item_model, ci_db_main_reference_model] }
+ let(:legacy_tables_models) do
+ [ci_db_main_item_model, ci_db_main_reference_model, ci_db_partitioned_item, ci_db_partitioned_item_detached]
+ end
+
let(:referencing_table_model) { ci_db_main_reference_model }
let(:referenced_table_model) { ci_db_main_item_model }
let(:other_tables_models) do
diff --git a/spec/lib/gitlab/database/transaction/context_spec.rb b/spec/lib/gitlab/database/transaction/context_spec.rb
index 33a47150060..1681098e20c 100644
--- a/spec/lib/gitlab/database/transaction/context_spec.rb
+++ b/spec/lib/gitlab/database/transaction/context_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::Transaction::Context do
+RSpec.describe Gitlab::Database::Transaction::Context, feature_category: :database do
subject { described_class.new }
let(:data) { subject.context }
diff --git a/spec/lib/gitlab/database/type/indifferent_jsonb_spec.rb b/spec/lib/gitlab/database/type/indifferent_jsonb_spec.rb
new file mode 100644
index 00000000000..6d27cbe180d
--- /dev/null
+++ b/spec/lib/gitlab/database/type/indifferent_jsonb_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Type::IndifferentJsonb do
+ let(:type) { described_class.new }
+
+ describe '#deserialize' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { type.deserialize(json) }
+
+ where(:json, :value) do
+ nil | nil
+ '{"key":"value"}' | { key: 'value' }
+ '{"key":[1,2,3]}' | { key: [1, 2, 3] }
+ '{"key":{"subkey":"value"}}' | { key: { subkey: 'value' } }
+ '{"key":{"a":[{"b":"c"},{"d":"e"}]}}' | { key: { a: [{ b: 'c' }, { d: 'e' }] } }
+ end
+
+ with_them do
+ it { is_expected.to match(value) }
+ it { is_expected.to match(value&.deep_stringify_keys) }
+ end
+ end
+
+ context 'when used by a model' do
+ let(:model) do
+ Class.new(ApplicationRecord) do
+ self.table_name = :_test_indifferent_jsonb
+
+ attribute :options, :ind_jsonb
+ end
+ end
+
+ let(:record) do
+ model.create!(name: 'test', options: { key: 'value' })
+ end
+
+ before do
+ model.connection.execute(<<~SQL)
+ CREATE TABLE _test_indifferent_jsonb(
+ id serial NOT NULL PRIMARY KEY,
+ name text,
+ options jsonb);
+ SQL
+
+ model.reset_column_information
+ end
+
+ it { expect(record.options).to match({ key: 'value' }) }
+ it { expect(record.options).to match({ 'key' => 'value' }) }
+
+ it 'ignores changes to other attributes' do
+ record.name = 'other test'
+
+ expect(record.changes).to match('name' => ['test', 'other test'])
+ end
+
+ it 'tracks changes to options' do
+ record.options = { key: 'other value' }
+
+ expect(record.changes).to match('options' => [{ 'key' => 'value' }, { 'key' => 'other value' }])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer_spec.rb b/spec/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer_spec.rb
new file mode 100644
index 00000000000..d8173794b3f
--- /dev/null
+++ b/spec/lib/gitlab/database_importers/work_items/hierarchy_restrictions_importer_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter,
+ feature_category: :portfolio_management do
+ subject { described_class.upsert_restrictions }
+
+ it_behaves_like 'work item hierarchy restrictions importer'
+end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index c788022bd3a..1a482b33a92 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -139,7 +139,7 @@ RSpec.describe Gitlab::Database do
describe '.db_config_for_connection' do
context 'when the regular connection is used' do
it 'returns db_config' do
- connection = ActiveRecord::Base.retrieve_connection
+ connection = ApplicationRecord.retrieve_connection
expect(described_class.db_config_for_connection(connection)).to eq(connection.pool.db_config)
end
@@ -147,12 +147,15 @@ RSpec.describe Gitlab::Database do
context 'when the connection is LoadBalancing::ConnectionProxy', :database_replica do
it 'returns primary db config even if ambiguous queries default to replica' do
- Gitlab::Database::LoadBalancing::Session.current.use_primary!
- primary_config = described_class.db_config_for_connection(ActiveRecord::Base.connection)
-
- Gitlab::Database::LoadBalancing::Session.clear_session
- Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
- expect(described_class.db_config_for_connection(ActiveRecord::Base.connection)).to eq(primary_config)
+ Gitlab::Database.database_base_models_using_load_balancing.each_value do |database_base_model|
+ connection = database_base_model.connection
+ Gitlab::Database::LoadBalancing::Session.current.use_primary!
+ primary_config = described_class.db_config_for_connection(connection)
+
+ Gitlab::Database::LoadBalancing::Session.clear_session
+ Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ expect(described_class.db_config_for_connection(connection)).to eq(primary_config)
+ end
end
end
end
@@ -180,11 +183,16 @@ RSpec.describe Gitlab::Database do
end
context 'when replicas are configured', :database_replica do
- it 'returns the name for a replica' do
- replica = ActiveRecord::Base.load_balancer.host
-
+ it 'returns the main_replica for a main database replica' do
+ replica = ApplicationRecord.load_balancer.host
expect(described_class.db_config_name(replica)).to eq('main_replica')
end
+
+ it 'returns the ci_replica for a ci database replica' do
+ skip_if_multiple_databases_not_setup
+ replica = Ci::ApplicationRecord.load_balancer.host
+ expect(described_class.db_config_name(replica)).to eq('ci_replica')
+ end
end
end
@@ -214,13 +222,17 @@ RSpec.describe Gitlab::Database do
expect(described_class.gitlab_schemas_for_connection(Ci::Build.connection)).to include(:gitlab_ci, :gitlab_shared)
end
+ # rubocop:disable Database/MultipleDatabases
it 'does return gitlab_ci when a ActiveRecord::Base is using CI connection' do
with_reestablished_active_record_base do
reconfigure_db_connection(model: ActiveRecord::Base, config_model: Ci::Build)
- expect(described_class.gitlab_schemas_for_connection(ActiveRecord::Base.connection)).to include(:gitlab_ci, :gitlab_shared)
+ expect(
+ described_class.gitlab_schemas_for_connection(ActiveRecord::Base.connection)
+ ).to include(:gitlab_ci, :gitlab_shared)
end
end
+ # rubocop:enable Database/MultipleDatabases
it 'does return a valid schema for a replica connection' do
with_replica_pool_for(ActiveRecord::Base) do |main_replica_pool|
@@ -281,7 +293,8 @@ RSpec.describe Gitlab::Database do
it 'does return empty for non-adopted connections' do
new_connection = ActiveRecord::Base.postgresql_connection(
- ActiveRecord::Base.connection_db_config.configuration_hash)
+ ActiveRecord::Base.connection_db_config.configuration_hash # rubocop:disable Database/MultipleDatabases
+ )
expect(described_class.gitlab_schemas_for_connection(new_connection)).to be_nil
ensure
@@ -405,7 +418,7 @@ RSpec.describe Gitlab::Database do
context 'within a transaction block' do
it 'publishes a transaction event' do
events = subscribe_events do
- ActiveRecord::Base.transaction do
+ ApplicationRecord.transaction do
User.first
end
end
@@ -424,10 +437,11 @@ RSpec.describe Gitlab::Database do
context 'within an empty transaction block' do
it 'publishes a transaction event' do
events = subscribe_events do
- ActiveRecord::Base.transaction {}
+ ApplicationRecord.transaction {}
+ Ci::ApplicationRecord.transaction {}
end
- expect(events.length).to be(1)
+ expect(events.length).to be(2)
event = events.first
expect(event).not_to be_nil
@@ -441,9 +455,9 @@ RSpec.describe Gitlab::Database do
context 'within a nested transaction block' do
it 'publishes multiple transaction events' do
events = subscribe_events do
- ActiveRecord::Base.transaction do
- ActiveRecord::Base.transaction do
- ActiveRecord::Base.transaction do
+ ApplicationRecord.transaction do
+ ApplicationRecord.transaction do
+ ApplicationRecord.transaction do
User.first
end
end
@@ -465,7 +479,7 @@ RSpec.describe Gitlab::Database do
context 'within a cancelled transaction block' do
it 'publishes multiple transaction events' do
events = subscribe_events do
- ActiveRecord::Base.transaction do
+ ApplicationRecord.transaction do
User.first
raise ActiveRecord::Rollback
end
diff --git a/spec/lib/gitlab/diff/file_collection/compare_spec.rb b/spec/lib/gitlab/diff/file_collection/compare_spec.rb
index ce70903a480..c3f768db7f0 100644
--- a/spec/lib/gitlab/diff/file_collection/compare_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/compare_spec.rb
@@ -16,10 +16,11 @@ RSpec.describe Gitlab::Diff::FileCollection::Compare do
end
let(:diffable) { Compare.new(raw_compare, project) }
+ let(:diff_options) { {} }
let(:collection_default_args) do
{
project: diffable.project,
- diff_options: {},
+ diff_options: diff_options,
diff_refs: diffable.diff_refs
}
end
@@ -65,4 +66,32 @@ RSpec.describe Gitlab::Diff::FileCollection::Compare do
expect(cache_key).to eq ['compare', head_commit.id, start_commit.id]
end
end
+
+ describe 'pagination methods' do
+ subject(:compare) { described_class.new(diffable, **collection_default_args) }
+
+ context 'when pagination options are not present' do
+ it 'returns default values' do
+ expect(compare.limit_value).to eq(Kaminari.config.default_per_page)
+ expect(compare.current_page).to eq(1)
+ expect(compare.next_page).to be_nil
+ expect(compare.prev_page).to be_nil
+ expect(compare.total_count).to be_nil
+ expect(compare.total_pages).to eq(0)
+ end
+ end
+
+ context 'when pagination options are present' do
+ let(:diff_options) { { page: 1, per_page: 10, count: 20 } }
+
+ it 'returns values based on options' do
+ expect(compare.limit_value).to eq(10)
+ expect(compare.current_page).to eq(1)
+ expect(compare.next_page).to eq(2)
+ expect(compare.prev_page).to be_nil
+ expect(compare.total_count).to eq(20)
+ expect(compare.total_pages).to eq(2)
+ end
+ end
+ end
end
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 beb85d383a0..9ac242459bf 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do
+RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch, feature_category: :code_review do
let(:merge_request) { create(:merge_request) }
let(:batch_page) { 0 }
let(:batch_size) { 10 }
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
new file mode 100644
index 00000000000..74e5e667702
--- /dev/null
+++ b/spec/lib/gitlab/diff/file_collection/paginated_merge_request_diff_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff, feature_category: :code_review do
+ let(:merge_request) { create(:merge_request) }
+ let(:page) { 1 }
+ let(:per_page) { 10 }
+ let(:diffable) { merge_request.merge_request_diff }
+ let(:diff_files_relation) { diffable.merge_request_diff_files }
+ let(:diff_files) { subject.diff_files }
+
+ subject do
+ described_class.new(diffable,
+ page,
+ per_page)
+ end
+
+ describe '#diff_files' do
+ let(:per_page) { 3 }
+ let(:paginated_rel) { diff_files_relation.page(page).per(per_page) }
+
+ let(:expected_batch_files) do
+ paginated_rel.map(&:new_path)
+ end
+
+ it 'returns paginated diff files' do
+ expect(diff_files.size).to eq(3)
+ end
+
+ it 'returns a valid instance of a DiffCollection' do
+ expect(diff_files).to be_a(Gitlab::Git::DiffCollection)
+ end
+
+ context 'when first page' do
+ it 'returns correct diff files' do
+ expect(diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+
+ context 'when another page' do
+ let(:page) { 2 }
+
+ it 'returns correct diff files' do
+ expect(diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+
+ context 'when page is nil' do
+ let(:page) { nil }
+
+ it 'returns correct diff files' do
+ expected_batch_files =
+ diff_files_relation.page(described_class::DEFAULT_PAGE).per(per_page).map(&:new_path)
+
+ expect(diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+
+ context 'when per_page is nil' do
+ let(:per_page) { nil }
+
+ it 'returns correct diff files' do
+ expected_batch_files =
+ diff_files_relation.page(page).per(described_class::DEFAULT_PER_PAGE).map(&:new_path)
+
+ expect(diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+
+ context 'when invalid page' do
+ let(:page) { 999 }
+
+ it 'returns correct diff files' do
+ expect(diff_files.map(&:new_path)).to be_empty
+ end
+ end
+
+ 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)
+
+ expected_batch_files = diff_files_relation.page(last_page).per(per_page).map(&:new_path)
+
+ expect(collection.diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+ end
+
+ it_behaves_like 'unfoldable diff' do
+ subject do
+ described_class.new(merge_request.merge_request_diff,
+ page,
+ per_page)
+ end
+ end
+
+ it_behaves_like 'cacheable diff collection' do
+ let(:cacheable_files_count) { per_page }
+ end
+
+ it_behaves_like 'unsortable diff files' do
+ let(:diffable) { merge_request.merge_request_diff }
+
+ subject do
+ described_class.new(merge_request.merge_request_diff,
+ page,
+ per_page)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
index 75538baf07f..8ff8de2379a 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Email::Handler::CreateIssueHandler do
- include_context :email_shared_context
+ include_context 'email shared context'
let!(:user) do
create(
:user,
@@ -16,13 +16,13 @@ RSpec.describe Gitlab::Email::Handler::CreateIssueHandler do
let(:namespace) { create(:namespace, path: 'gitlabhq') }
let(:email_raw) { email_fixture('emails/valid_new_issue.eml') }
- it_behaves_like :reply_processing_shared_examples
-
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
stub_config_setting(host: 'localhost')
end
+ it_behaves_like 'reply processing shared examples'
+
context "when email key" do
let(:mail) { Mail::Message.new(email_raw) }
diff --git a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
index 37ee4591db0..f5b44d30c50 100644
--- a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Email::Handler::CreateMergeRequestHandler do
- include_context :email_shared_context
+ include_context 'email shared context'
let!(:user) do
create(
:user,
@@ -16,16 +16,16 @@ RSpec.describe Gitlab::Email::Handler::CreateMergeRequestHandler do
let(:namespace) { create(:namespace, path: 'gitlabhq') }
let(:email_raw) { email_fixture('emails/valid_new_merge_request.eml') }
- it_behaves_like :reply_processing_shared_examples
+ after do
+ TestEnv.clean_test_path
+ end
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
stub_config_setting(host: 'localhost')
end
- after do
- TestEnv.clean_test_path
- end
+ it_behaves_like 'reply processing shared examples'
context "when email key" do
let(:mail) { Mail::Message.new(email_raw) }
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index 585dce331ed..f70645a8272 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
- include_context :email_shared_context
+ include_context 'email shared context'
let_it_be(:user) { create(:user, email: 'jake@adventuretime.ooo') }
let_it_be(:project) { create(:project, :public, :repository) }
@@ -15,9 +15,14 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
SentNotification.record_note(note, user.id, mail_key)
end
- it_behaves_like :reply_processing_shared_examples
+ before do
+ stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
+ stub_config_setting(host: 'localhost')
+ end
+
+ it_behaves_like 'reply processing shared examples'
- it_behaves_like :note_handler_shared_examples do
+ it_behaves_like 'note handler shared examples' do
let(:recipient) { sent_notification.recipient }
let(:update_commands_only) { fixture_file('emails/update_commands_only_reply.eml') }
@@ -26,11 +31,6 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
let(:with_quick_actions) { fixture_file('emails/valid_reply_with_quick_actions.eml') }
end
- before do
- stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
- stub_config_setting(host: 'localhost')
- end
-
context 'when the recipient address does not include a mail key' do
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, '') }
@@ -92,7 +92,7 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
issue.update_attribute(:confidential, true)
end
- it_behaves_like :checks_permissions_on_noteable_examples
+ it_behaves_like 'checks permissions on noteable examples'
end
shared_examples 'a reply to existing comment' do
diff --git a/spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb
index d3535fa9bd3..6e83c06c1b4 100644
--- a/spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Email::Handler::CreateNoteOnIssuableHandler do
- include_context :email_shared_context
+ include_context 'email shared context'
let_it_be(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') }
let_it_be(:namespace) { create(:namespace, path: 'gitlabhq') }
@@ -17,9 +17,9 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteOnIssuableHandler do
stub_config_setting(host: 'localhost')
end
- it_behaves_like :reply_processing_shared_examples
+ it_behaves_like 'reply processing shared examples'
- it_behaves_like :note_handler_shared_examples, true do
+ it_behaves_like 'note handler shared examples', true do
let_it_be(:recipient) { user }
let(:update_commands_only) { email_reply_fixture('emails/update_commands_only.eml') }
@@ -42,7 +42,7 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteOnIssuableHandler do
noteable.update_attribute(:confidential, true)
end
- it_behaves_like :checks_permissions_on_noteable_examples
+ it_behaves_like 'checks permissions on noteable examples'
end
def email_fixture(path)
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 08a7383700b..7bba0775668 100644
--- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
- include_context :email_shared_context
+ include ServiceDeskHelper
+ include_context 'email shared context'
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
@@ -184,12 +185,6 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
context 'and template is present' do
let_it_be(:settings) { create(:service_desk_setting, project: project) }
- def set_template_file(file_name, content)
- file_path = ".gitlab/issue_templates/#{file_name}.md"
- project.repository.create_file(user, file_path, content, message: 'message', branch_name: 'master')
- settings.update!(issue_template_key: file_name)
- end
-
it 'appends template text to issue description' do
set_template_file('service_desk', 'text from template')
diff --git a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
index 2bc3cd81b48..f33e9eba5c6 100644
--- a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Email::Handler::UnsubscribeHandler do
- include_context :email_shared_context
+ include_context 'email shared context'
before do
stub_incoming_email_setting(enabled: true, address: 'reply+%{key}@appmail.adventuretime.ooo')
diff --git a/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb b/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb
index 47f6015c6f8..b22c55208f0 100644
--- a/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb
+++ b/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb
@@ -7,15 +7,15 @@ RSpec.describe Gitlab::Email::Hook::DisableEmailInterceptor do
Mail.register_interceptor(described_class)
end
+ after do
+ Mail.unregister_interceptor(described_class)
+ end
+
it 'does not send emails' do
allow(Gitlab.config.gitlab).to receive(:email_enabled).and_return(false)
expect { deliver_mail }.not_to change(ActionMailer::Base.deliveries, :count)
end
- after do
- Mail.unregister_interceptor(described_class)
- end
-
def deliver_mail
key = create :personal_key
Notify.new_ssh_key_email(key.id)
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 9240d07fd59..865e40d4ecb 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Email::Receiver do
- include_context :email_shared_context
+ include_context 'email shared context'
let_it_be(:project) { create(:project) }
let(:metric_transaction) { instance_double(Gitlab::Metrics::WebTransaction) }
diff --git a/spec/lib/gitlab/file_type_detection_spec.rb b/spec/lib/gitlab/file_type_detection_spec.rb
index c435d3f6097..1be0f7d53fa 100644
--- a/spec/lib/gitlab/file_type_detection_spec.rb
+++ b/spec/lib/gitlab/file_type_detection_spec.rb
@@ -31,6 +31,7 @@ RSpec.describe Gitlab::FileTypeDetection do
expect(described_class.extension_match?('my/file.foo', extensions)).to eq(true)
end
end
+
context 'when class is an uploader' do
let(:uploader) do
example_uploader = Class.new(CarrierWave::Uploader::Base) do
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
index b1bff242f33..e1c0da69317 100644
--- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::Gfm::UploadsRewriter do
referenced_files.compact.select(&:exists?)
end
- shared_examples "files are accessible" do
+ shared_examples 'files are accessible' do
describe '#rewrite' do
subject(:rewrite) { new_text }
@@ -82,6 +82,18 @@ RSpec.describe Gitlab::Gfm::UploadsRewriter do
rewrite
expect(new_files).to be_empty
+ expect(new_text).to eq(text)
+ end
+
+ it 'skips non-existant files' do
+ allow_next_instance_of(FileUploader) do |file|
+ allow(file).to receive(:exists?).and_return(false)
+ end
+
+ rewrite
+
+ expect(new_files).to be_empty
+ expect(new_text).to eq(text)
end
end
end
@@ -107,11 +119,11 @@ RSpec.describe Gitlab::Gfm::UploadsRewriter do
end
end
- context "file are stored locally" do
- include_examples "files are accessible"
+ context 'file are stored locally' do
+ include_examples 'files are accessible'
end
- context "files are stored remotely" do
+ context 'files are stored remotely' do
before do
stub_uploads_object_storage(FileUploader)
@@ -120,7 +132,7 @@ RSpec.describe Gitlab::Gfm::UploadsRewriter do
end
end
- include_examples "files are accessible"
+ include_examples 'files are accessible'
end
describe '#needs_rewrite?' do
diff --git a/spec/lib/gitlab/git/base_error_spec.rb b/spec/lib/gitlab/git/base_error_spec.rb
index 851cfa16512..d4db7cf2430 100644
--- a/spec/lib/gitlab/git/base_error_spec.rb
+++ b/spec/lib/gitlab/git/base_error_spec.rb
@@ -20,4 +20,15 @@ RSpec.describe Gitlab::Git::BaseError do
with_them do
it { is_expected.to eq(result) }
end
+
+ describe "When initialized with GRPC errors" do
+ let(:grpc_error) { GRPC::DeadlineExceeded.new }
+ let(:git_error) { described_class.new grpc_error }
+
+ it "has status and code fields" do
+ expect(git_error.service).to eq('git')
+ expect(git_error.status).to eq(4)
+ expect(git_error.code).to eq('deadline_exceeded')
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/cross_repo_comparer_spec.rb b/spec/lib/gitlab/git/cross_repo_comparer_spec.rb
deleted file mode 100644
index 7888e224d59..00000000000
--- a/spec/lib/gitlab/git/cross_repo_comparer_spec.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Git::CrossRepoComparer do
- let(:source_project) { create(:project, :repository) }
- let(:target_project) { create(:project, :repository) }
-
- let(:source_repo) { source_project.repository.raw_repository }
- let(:target_repo) { target_project.repository.raw_repository }
-
- let(:source_branch) { 'feature' }
- let(:target_branch) { 'master' }
- let(:straight) { false }
-
- let(:source_commit) { source_repo.commit(source_branch) }
- let(:target_commit) { source_repo.commit(target_branch) }
-
- subject(:result) { described_class.new(source_repo, target_repo).compare(source_branch, target_branch, straight: straight) }
-
- describe '#compare' do
- context 'within a single repository' do
- let(:target_project) { source_project }
-
- context 'a non-straight comparison' do
- it 'compares without fetching from another repo' do
- expect(source_repo).not_to receive(:fetch_source_branch!)
-
- expect_compare(result, from: source_commit, to: target_commit)
- expect(result.straight).to eq(false)
- end
- end
-
- context 'a straight comparison' do
- let(:straight) { true }
-
- it 'compares without fetching from another repo' do
- expect(source_repo).not_to receive(:fetch_source_branch!)
-
- expect_compare(result, from: source_commit, to: target_commit)
- expect(result.straight).to eq(true)
- end
- end
- end
-
- context 'across two repositories' do
- context 'target ref exists in source repo' do
- it 'compares without fetching from another repo' do
- expect(source_repo).not_to receive(:fetch_source_branch!)
- expect(source_repo).not_to receive(:delete_refs)
-
- expect_compare(result, from: source_commit, to: target_commit)
- end
- end
-
- context 'target ref does not exist in source repo' do
- it 'compares in the source repo by fetching from the target to a temporary ref' do
- new_commit_id = create_commit(target_project.owner, target_repo, target_branch)
- new_commit = target_repo.commit(new_commit_id)
-
- # This is how the temporary ref is generated
- expect(SecureRandom).to receive(:hex).at_least(:once).and_return('foo')
-
- expect(source_repo)
- .to receive(:fetch_source_branch!)
- .with(target_repo, new_commit_id, 'refs/tmp/foo')
- .and_call_original
-
- expect(source_repo).to receive(:delete_refs).with('refs/tmp/foo').and_call_original
-
- expect_compare(result, from: source_commit, to: new_commit)
- end
- end
-
- context 'source ref does not exist in source repo' do
- let(:source_branch) { 'does-not-exist' }
-
- it 'returns an empty comparison' do
- expect(source_repo).not_to receive(:fetch_source_branch!)
- expect(source_repo).not_to receive(:delete_refs)
-
- expect(result).to be_a(::Gitlab::Git::Compare)
- expect(result.commits.size).to eq(0)
- end
- end
-
- context 'target ref does not exist in target repo' do
- let(:target_branch) { 'does-not-exist' }
-
- it 'returns nil' do
- expect(source_repo).not_to receive(:fetch_source_branch!)
- expect(source_repo).not_to receive(:delete_refs)
-
- is_expected.to be_nil
- end
- end
- end
- end
-
- def expect_compare(of, from:, to:)
- expect(of).to be_a(::Gitlab::Git::Compare)
- expect(from).to be_a(::Gitlab::Git::Commit)
- expect(to).to be_a(::Gitlab::Git::Commit)
-
- expect(of.commits).not_to be_empty
- expect(of.head).to eq(from)
- expect(of.base).to eq(to)
- end
-
- def create_commit(user, repo, branch)
- action = { action: :create, file_path: '/FILE', content: 'content' }
-
- result = repo.commit_files(user, branch_name: branch, message: 'Commit', actions: [action])
-
- result.newrev
- end
-end
diff --git a/spec/lib/gitlab/git/cross_repo_spec.rb b/spec/lib/gitlab/git/cross_repo_spec.rb
new file mode 100644
index 00000000000..09a28c144a4
--- /dev/null
+++ b/spec/lib/gitlab/git/cross_repo_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Git::CrossRepo do
+ let_it_be(:source_project) { create(:project, :repository) }
+ let_it_be(:target_project) { create(:project, :repository) }
+
+ let(:source_repo) { source_project.repository.raw_repository }
+ let(:target_repo) { target_project.repository.raw_repository }
+
+ let(:source_branch) { 'feature' }
+ let(:target_branch) { target_repo.root_ref }
+
+ let(:source_commit) { source_repo.commit(source_branch) }
+ let(:target_commit) { source_repo.commit(target_branch) }
+
+ def execute(&block)
+ described_class.new(source_repo, target_repo).execute(target_branch, &block)
+ end
+
+ describe '#execute' do
+ context 'when executed within a single repository' do
+ let(:target_project) { source_project }
+
+ it 'does not fetch from another repo' do
+ expect(source_repo).not_to receive(:fetch_source_branch!)
+
+ expect { |block| execute(&block) }.to yield_with_args(target_branch)
+ end
+ end
+
+ context 'when executed across two repositories' do
+ context 'and target ref exists in source repo' do
+ it 'does not fetch from another repo' do
+ expect(source_repo).not_to receive(:fetch_source_branch!)
+ expect(source_repo).not_to receive(:delete_refs)
+
+ expect { |block| execute(&block) }.to yield_with_args(target_commit.id)
+ end
+ end
+
+ context 'and target ref does not exist in source repo' do
+ let_it_be(:target_project) { create(:project, :repository) }
+
+ it 'fetches from the target to a temporary ref' do
+ new_commit_id = create_commit(target_project.owner, target_repo, target_branch)
+
+ # This is how the temporary ref is generated
+ expect(SecureRandom).to receive(:hex).at_least(:once).and_return('foo')
+
+ expect(source_repo)
+ .to receive(:fetch_source_branch!)
+ .with(target_repo, new_commit_id, 'refs/tmp/foo')
+ .and_call_original
+
+ expect(source_repo).to receive(:delete_refs).with('refs/tmp/foo').and_call_original
+
+ expect { |block| execute(&block) }.to yield_with_args(new_commit_id)
+ end
+ end
+
+ context 'and target ref does not exist in target repo' do
+ let(:target_branch) { 'does-not-exist' }
+
+ it 'returns nil' do
+ expect(source_repo).not_to receive(:fetch_source_branch!)
+ expect(source_repo).not_to receive(:delete_refs)
+
+ expect { |block| execute(&block) }.not_to yield_control
+ end
+ end
+ end
+ end
+
+ def create_commit(user, repo, branch)
+ action = { action: :create, file_path: '/FILE', content: 'content' }
+
+ result = repo.commit_files(user, branch_name: branch, message: 'Commit', actions: [action])
+
+ result.newrev
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 197662943a0..6cff39c1167 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Gitlab::Git::Repository do
+RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_management do
include Gitlab::EncodingHelper
include RepoHelpers
using RSpec::Parameterized::TableSyntax
@@ -70,12 +70,7 @@ RSpec.describe Gitlab::Git::Repository do
it { is_expected.to include("master") }
it { is_expected.not_to include("branch-from-space") }
- it 'gets the branch names from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:branch_names)
- subject
- end
-
- it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :branch_names
+ it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :list_refs
end
describe '#tag_names' do
@@ -100,7 +95,7 @@ RSpec.describe Gitlab::Git::Repository do
it { is_expected.to include("v1.0.0") }
it { is_expected.not_to include("v5.0.0") }
- it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tag_names
+ it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :list_refs
end
describe '#tags' do
@@ -1353,7 +1348,7 @@ RSpec.describe Gitlab::Git::Repository do
it "returns the number of commits in the whole repository" do
options = { all: true }
- expect(repository.count_commits(options)).to eq(314)
+ expect(repository.count_commits(options)).to eq(315)
end
end
@@ -1378,6 +1373,24 @@ RSpec.describe Gitlab::Git::Repository do
expect(branch).to eq(nil)
end
+
+ context 'when branch is ambiguous' do
+ let(:ambiguous_branch) { 'prefix' }
+ let(:branch_with_prefix) { 'prefix/branch' }
+
+ before do
+ repository.create_branch(branch_with_prefix)
+ end
+
+ after do
+ repository.delete_branch(branch_with_prefix)
+ end
+
+ it 'returns nil for ambiguous branch' do
+ expect(repository.find_branch(branch_with_prefix)).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(repository.find_branch(ambiguous_branch)).to eq(nil)
+ end
+ end
end
describe '#branches' do
@@ -1416,16 +1429,6 @@ RSpec.describe Gitlab::Git::Repository do
it 'returns the count of local branches' do
expect(repository.branch_count).to eq(repository.local_branches.count)
end
-
- context 'with Gitaly disabled' do
- before do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false)
- end
-
- it 'returns the count of local branches' do
- expect(repository.branch_count).to eq(repository.local_branches.count)
- end
- end
end
end
@@ -2212,15 +2215,49 @@ RSpec.describe Gitlab::Git::Repository do
end
describe '#compare_source_branch' do
- it 'delegates to Gitlab::Git::CrossRepoComparer' do
- expect_next_instance_of(::Gitlab::Git::CrossRepoComparer) do |instance|
- expect(instance.source_repo).to eq(:source_repository)
- expect(instance.target_repo).to eq(repository)
+ it 'compares two branches cross repo' do
+ mutable_repository.commit_files(
+ user,
+ branch_name: mutable_repository.root_ref, message: 'Committing something',
+ actions: [{ action: :create, file_path: 'encoding/CHANGELOG', content: 'New file' }]
+ )
+
+ repository.commit_files(
+ user,
+ branch_name: repository.root_ref, message: 'Commit to root ref',
+ actions: [{ action: :create, file_path: 'encoding/CHANGELOG', content: 'One more' }]
+ )
- expect(instance).to receive(:compare).with('feature', 'master', straight: :straight)
+ [
+ [repository, mutable_repository, true],
+ [repository, mutable_repository, false],
+ [mutable_repository, repository, true],
+ [mutable_repository, repository, false]
+ ].each do |source_repo, target_repo, straight|
+ raw_compare = target_repo.compare_source_branch(
+ target_repo.root_ref, source_repo, source_repo.root_ref, straight: straight)
+
+ expect(raw_compare).to be_a(::Gitlab::Git::Compare)
+
+ expect(raw_compare.commits).to eq([source_repo.commit])
+ expect(raw_compare.head).to eq(source_repo.commit)
+ expect(raw_compare.base).to eq(target_repo.commit)
+ expect(raw_compare.straight).to eq(straight)
end
+ end
+
+ context 'source ref does not exist in source repo' do
+ it 'returns an empty comparison' do
+ expect_next_instance_of(::Gitlab::Git::CrossRepo) do |instance|
+ expect(instance).not_to receive(:fetch_source_branch!)
+ end
+
+ raw_compare = repository.compare_source_branch(
+ repository.root_ref, mutable_repository, 'does-not-exist', straight: true)
- repository.compare_source_branch('master', :source_repository, 'feature', straight: :straight)
+ expect(raw_compare).to be_a(::Gitlab::Git::Compare)
+ expect(raw_compare.commits.size).to eq(0)
+ end
end
end
@@ -2517,4 +2554,30 @@ RSpec.describe Gitlab::Git::Repository do
end
end
end
+
+ describe '#check_objects_exist' do
+ it 'returns hash specifying which object exists in repo' do
+ refs_exist = %w(
+ b83d6e391c22777fca1ed3012fce84f633d7fed0
+ 498214de67004b1da3d820901307bed2a68a8ef6
+ 1b12f15a11fc6e62177bef08f47bc7b5ce50b141
+ )
+ 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,
+ '498214de67004b1da3d820901307bed2a68a8ef6' => true,
+ '1b12f15a11fc6e62177bef08f47bc7b5ce50b141' => true,
+ '1111111111111111111111111111111111111111' => false,
+ '2222222222222222222222222222222222222222' => false
+ })
+ expect(object_existence_map.keys).to eq(refs_exist + refs_dont_exist)
+
+ single_sha = 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
+ expect(repository.check_objects_exist(single_sha)).to eq({ single_sha => true })
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
index 524b373a5b7..1b8da0b380b 100644
--- a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
+++ b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require 'json'
require 'tempfile'
-RSpec.describe Gitlab::Git::RuggedImpl::UseRugged do
+RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, feature_category: :gitlay do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:feature_flag_name) { wrapper.rugged_feature_keys.first }
@@ -18,8 +18,7 @@ RSpec.describe Gitlab::Git::RuggedImpl::UseRugged do
klazz = Class.new do
include Gitlab::Git::RuggedImpl::UseRugged
- def rugged_test(ref, test_number)
- end
+ def rugged_test(ref, test_number); end
end
klazz.new
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index 17f802b9f66..2a68fa66b18 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Git::Tree do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw }
- shared_examples :repo do
+ shared_examples 'repo' do
subject(:tree) { Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, pagination_params) }
let(:sha) { SeedRepo::Commit::ID }
@@ -151,7 +151,7 @@ RSpec.describe Gitlab::Git::Tree do
end
describe '.where with Gitaly enabled' do
- it_behaves_like :repo do
+ it_behaves_like 'repo' do
context 'with pagination parameters' do
let(:pagination_params) { { limit: 3, page_token: nil } }
@@ -172,7 +172,7 @@ RSpec.describe Gitlab::Git::Tree do
described_class.where(repository, SeedRepo::Commit::ID, 'files', false, false)
end
- it_behaves_like :repo do
+ it_behaves_like 'repo' do
describe 'Pagination' do
context 'with restrictive limit' do
let(:pagination_params) { { limit: 3, page_token: nil } }
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index 604feeea325..82d5d0f292b 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -898,7 +898,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
end
shared_examples '#user_commit_files failure' do
- it 'raises a PreReceiveError' do
+ it 'raises an IndexError' do
expect_any_instance_of(Gitaly::OperationService::Stub)
.to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash))
.and_raise(structured_error)
@@ -912,7 +912,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
context 'with missing file' do
let(:status_code) { GRPC::Core::StatusCodes::NOT_FOUND }
- let(:expected_message) { "File not found: README.md" }
+ let(:expected_message) { "A file with this name doesn't exist" }
let(:expected_error) do
Gitaly::UserCommitFilesError.new(
index_update: Gitaly::IndexError.new(
@@ -926,7 +926,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
context 'with existing directory' do
let(:status_code) { GRPC::Core::StatusCodes::ALREADY_EXISTS }
- let(:expected_message) { "Directory already exists: dir1" }
+ let(:expected_message) { "A directory with this name already exists" }
let(:expected_error) do
Gitaly::UserCommitFilesError.new(
index_update: Gitaly::IndexError.new(
@@ -940,7 +940,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
context 'with existing file' do
let(:status_code) { GRPC::Core::StatusCodes::ALREADY_EXISTS }
- let(:expected_message) { "File already exists: README.md" }
+ let(:expected_message) { "A file with this name already exists" }
let(:expected_error) do
Gitaly::UserCommitFilesError.new(
index_update: Gitaly::IndexError.new(
@@ -954,7 +954,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
context 'with invalid path' do
let(:status_code) { GRPC::Core::StatusCodes::INVALID_ARGUMENT }
- let(:expected_message) { "Invalid path: invalid://file/name" }
+ let(:expected_message) { "invalid path: 'invalid://file/name'" }
let(:expected_error) do
Gitaly::UserCommitFilesError.new(
index_update: Gitaly::IndexError.new(
@@ -968,7 +968,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
context 'with directory traversal' do
let(:status_code) { GRPC::Core::StatusCodes::INVALID_ARGUMENT }
- let(:expected_message) { "Directory traversal in path escapes repository: ../../../../etc/shadow" }
+ let(:expected_message) { "Path cannot include directory traversal" }
let(:expected_error) do
Gitaly::UserCommitFilesError.new(
index_update: Gitaly::IndexError.new(
@@ -982,7 +982,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
context 'with empty path' do
let(:status_code) { GRPC::Core::StatusCodes::INVALID_ARGUMENT }
- let(:expected_message) { "Received empty path" }
+ let(:expected_message) { "You must provide a file path" }
let(:expected_error) do
Gitaly::UserCommitFilesError.new(
index_update: Gitaly::IndexError.new(
@@ -1009,16 +1009,33 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
end
context 'with an exception without the detailed error' do
- let(:permission_error) do
- GRPC::PermissionDenied.new
- end
-
- it 'raises PermissionDenied' do
+ before do
expect_any_instance_of(Gitaly::OperationService::Stub)
.to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash))
- .and_raise(permission_error)
+ .and_raise(raised_error)
+ end
- expect { subject }.to raise_error(GRPC::PermissionDenied)
+ context 'with an index error from libgit2' do
+ let(:raised_error) do
+ GRPC::Internal.new('invalid path: .git/foo')
+ end
+
+ it 'raises IndexError' do
+ expect { subject }.to raise_error do |error|
+ expect(error).to be_a(Gitlab::Git::Index::IndexError)
+ expect(error.message).to eq('invalid path: .git/foo')
+ end
+ end
+ end
+
+ context 'with a generic error' do
+ let(:raised_error) do
+ GRPC::PermissionDenied.new
+ end
+
+ it 'raises PermissionDenied' do
+ expect { subject }.to raise_error(GRPC::PermissionDenied)
+ end
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index bd96e9baf1d..ae2e343377d 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -71,28 +71,6 @@ RSpec.describe Gitlab::GitalyClient::RefService do
end
end
- describe '#branch_names' do
- it 'sends a find_all_branch_names message' do
- expect_any_instance_of(Gitaly::RefService::Stub)
- .to receive(:find_all_branch_names)
- .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
- .and_return([])
-
- client.branch_names
- end
- end
-
- describe '#tag_names' do
- it 'sends a find_all_tag_names message' do
- expect_any_instance_of(Gitaly::RefService::Stub)
- .to receive(:find_all_tag_names)
- .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
- .and_return([])
-
- client.tag_names
- end
- end
-
describe '#find_branch' do
it 'sends a find_branch message' do
expect_any_instance_of(Gitaly::RefService::Stub)
@@ -102,6 +80,16 @@ RSpec.describe Gitlab::GitalyClient::RefService do
client.find_branch('name')
end
+
+ context 'when Gitaly returns a ambiguios reference error' do
+ it 'raises an UnknownRef error' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:find_branch)
+ .and_raise(GRPC::BadStatus.new(2, 'reference is ambiguous'))
+
+ expect { client.find_branch('name') }.to raise_error(Gitlab::Git::AmbiguousRef, 'branch is ambiguous: name')
+ end
+ end
end
describe '#find_tag' do
diff --git a/spec/lib/gitlab/gitaly_client/with_feature_flag_actors_spec.rb b/spec/lib/gitlab/gitaly_client/with_feature_flag_actors_spec.rb
index 41dce5d76dd..61945cc06b8 100644
--- a/spec/lib/gitlab/gitaly_client/with_feature_flag_actors_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/with_feature_flag_actors_spec.rb
@@ -157,70 +157,47 @@ RSpec.describe Gitlab::GitalyClient::WithFeatureFlagActors do
let(:call_arg_2) { double }
let(:call_arg_3) { double }
let(:call_result) { double }
+ let(:repository_actor) { instance_double(::Repository) }
+ let(:user_actor) { instance_double(::User) }
+ let(:project_actor) { instance_double(Project) }
+ let(:group_actor) { instance_double(Group) }
before do
+ allow(service).to receive(:user_actor).and_return(user_actor)
+ allow(service).to receive(:repository_actor).and_return(repository_actor)
+ allow(service).to receive(:project_actor).and_return(project_actor)
+ allow(service).to receive(:group_actor).and_return(group_actor)
+ allow(Gitlab::GitalyClient).to receive(:with_feature_flag_actors).and_call_original
allow(Gitlab::GitalyClient).to receive(:call).and_return(call_result)
end
- context 'when actors_aware_gitaly_calls flag is enabled' do
- let(:repository_actor) { instance_double(::Repository) }
- let(:user_actor) { instance_double(::User) }
- let(:project_actor) { instance_double(Project) }
- let(:group_actor) { instance_double(Group) }
-
- before do
- stub_feature_flags(actors_aware_gitaly_calls: true)
-
- allow(service).to receive(:user_actor).and_return(user_actor)
- allow(service).to receive(:repository_actor).and_return(repository_actor)
- allow(service).to receive(:project_actor).and_return(project_actor)
- allow(service).to receive(:group_actor).and_return(group_actor)
- allow(Gitlab::GitalyClient).to receive(:with_feature_flag_actors).and_call_original
- end
-
- it 'triggers client call with feature flag actors' do
- result = service.gitaly_client_call(call_arg_1, call_arg_2, karg: call_arg_3)
-
- expect(Gitlab::GitalyClient).to have_received(:call).with(call_arg_1, call_arg_2, karg: call_arg_3)
- expect(Gitlab::GitalyClient).to have_received(:with_feature_flag_actors).with(
- repository: repository_actor,
- user: user_actor,
- project: project_actor,
- group: group_actor
- )
- expect(result).to be(call_result)
- end
-
- context 'when call without repository_actor' do
- before do
- allow(service).to receive(:repository_actor).and_return(nil)
- allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).and_call_original
- end
-
- it 'calls error tracking track_and_raise_for_dev_exception' do
- expect do
- service.gitaly_client_call(call_arg_1, call_arg_2, karg: call_arg_3)
- end.to raise_error /gitaly_client_call called without setting repository_actor/
-
- expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception).with(
- be_a(Feature::InvalidFeatureFlagError)
- )
- end
- end
+ it 'triggers client call with feature flag actors' do
+ result = service.gitaly_client_call(call_arg_1, call_arg_2, karg: call_arg_3)
+
+ expect(Gitlab::GitalyClient).to have_received(:call).with(call_arg_1, call_arg_2, karg: call_arg_3)
+ expect(Gitlab::GitalyClient).to have_received(:with_feature_flag_actors).with(
+ repository: repository_actor,
+ user: user_actor,
+ project: project_actor,
+ group: group_actor
+ )
+ expect(result).to be(call_result)
end
- context 'when actors_aware_gitaly_calls not enabled' do
+ context 'when call without repository_actor' do
before do
- stub_feature_flags(actors_aware_gitaly_calls: false)
+ allow(service).to receive(:repository_actor).and_return(nil)
+ allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).and_call_original
end
- it 'triggers client call without feature flag actors' do
- expect(Gitlab::GitalyClient).not_to receive(:with_feature_flag_actors)
+ it 'calls error tracking track_and_raise_for_dev_exception' do
+ expect do
+ service.gitaly_client_call(call_arg_1, call_arg_2, karg: call_arg_3)
+ end.to raise_error /gitaly_client_call called without setting repository_actor/
- result = service.gitaly_client_call(call_arg_1, call_arg_2, karg: call_arg_3)
-
- expect(Gitlab::GitalyClient).to have_received(:call).with(call_arg_1, call_arg_2, karg: call_arg_3)
- expect(result).to be(call_result)
+ expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception).with(
+ be_a(Feature::InvalidFeatureFlagError)
+ )
end
end
@@ -228,47 +205,28 @@ RSpec.describe Gitlab::GitalyClient::WithFeatureFlagActors do
let_it_be(:project) { create(:project) }
let(:repository_actor) { project.repository }
- context 'when actors_aware_gitaly_calls flag is enabled' do
- let(:user_actor) { instance_double(::User) }
- let(:project_actor) { instance_double(Project) }
- let(:group_actor) { instance_double(Group) }
-
- before do
- stub_feature_flags(actors_aware_gitaly_calls: true)
-
- allow(Feature::Gitaly).to receive(:user_actor).and_return(user_actor)
- allow(Feature::Gitaly).to receive(:project_actor).with(project).and_return(project_actor)
- allow(Feature::Gitaly).to receive(:group_actor).with(project).and_return(group_actor)
- end
-
- it 'returns a hash with collected feature flag actors' do
- result = service.gitaly_feature_flag_actors(repository_actor)
- expect(result).to eql(
- repository: repository_actor,
- user: user_actor,
- project: project_actor,
- group: group_actor
- )
-
- expect(Feature::Gitaly).to have_received(:user_actor).with(no_args)
- expect(Feature::Gitaly).to have_received(:project_actor).with(project)
- expect(Feature::Gitaly).to have_received(:group_actor).with(project)
- end
- end
+ let(:user_actor) { instance_double(::User) }
+ let(:project_actor) { instance_double(Project) }
+ let(:group_actor) { instance_double(Group) }
- context 'when actors_aware_gitaly_calls not enabled' do
- before do
- stub_feature_flags(actors_aware_gitaly_calls: false)
- end
+ before do
+ allow(Feature::Gitaly).to receive(:user_actor).and_return(user_actor)
+ allow(Feature::Gitaly).to receive(:project_actor).with(project).and_return(project_actor)
+ allow(Feature::Gitaly).to receive(:group_actor).with(project).and_return(group_actor)
+ end
- it 'returns an empty hash' do
- expect(Feature::Gitaly).not_to receive(:user_actor)
- expect(Feature::Gitaly).not_to receive(:project_actor)
- expect(Feature::Gitaly).not_to receive(:group_actor)
+ it 'returns a hash with collected feature flag actors' do
+ result = service.gitaly_feature_flag_actors(repository_actor)
+ expect(result).to eql(
+ repository: repository_actor,
+ user: user_actor,
+ project: project_actor,
+ group: group_actor
+ )
- result = service.gitaly_feature_flag_actors(repository_actor)
- expect(result).to eql({})
- end
+ expect(Feature::Gitaly).to have_received(:user_actor).with(no_args)
+ expect(Feature::Gitaly).to have_received(:project_actor).with(project)
+ expect(Feature::Gitaly).to have_received(:group_actor).with(project)
end
end
end
diff --git a/spec/lib/gitlab/github_gists_import/importer/gist_importer_spec.rb b/spec/lib/gitlab/github_gists_import/importer/gist_importer_spec.rb
new file mode 100644
index 00000000000..69a4d646562
--- /dev/null
+++ b/spec/lib/gitlab/github_gists_import/importer/gist_importer_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubGistsImport::Importer::GistImporter, feature_category: :importer do
+ subject { described_class.new(gist_object, user.id).execute }
+
+ let_it_be(:user) { create(:user) }
+ let(:created_at) { Time.utc(2022, 1, 9, 12, 15) }
+ let(:updated_at) { Time.utc(2022, 5, 9, 12, 17) }
+ let(:gist_file) { { file_name: '_Summary.md', file_content: 'File content' } }
+ let(:url) { 'https://host.com/gistid.git' }
+ let(:gist_object) do
+ instance_double('Gitlab::GithubGistsImport::Representation::Gist',
+ truncated_title: 'My Gist',
+ visibility_level: 0,
+ files: { '_Summary.md': gist_file },
+ first_file: gist_file,
+ git_pull_url: url,
+ created_at: created_at,
+ updated_at: updated_at
+ )
+ end
+
+ let(:expected_snippet_attrs) do
+ {
+ title: 'My Gist',
+ visibility_level: 0,
+ content: 'File content',
+ file_name: '_Summary.md',
+ author_id: user.id,
+ created_at: gist_object.created_at,
+ updated_at: gist_object.updated_at
+ }.stringify_keys
+ end
+
+ describe '#execute' do
+ context 'when success' do
+ it 'creates expected snippet and snippet repository' do
+ expect_next_instance_of(Repository) do |repository|
+ expect(repository).to receive(:fetch_as_mirror)
+ end
+
+ expect { subject }.to change { user.snippets.count }.by(1)
+ expect(user.snippets[0].attributes).to include expected_snippet_attrs
+ end
+ end
+
+ context 'when file size limit exeeded' do
+ before do
+ files = [].tap { |array| 11.times { |n| array << ["file#{n}.txt", {}] } }.to_h
+
+ allow(gist_object).to receive(:files).and_return(files)
+ allow_next_instance_of(Repository) do |repository|
+ allow(repository).to receive(:fetch_as_mirror)
+ allow(repository).to receive(:empty?).and_return(false)
+ allow(repository).to receive(:ls_files).and_return(files.keys)
+ end
+ end
+
+ it 'returns error' do
+ result = subject
+
+ expect(user.snippets.count).to eq(0)
+ expect(result.error?).to eq(true)
+ expect(result.errors).to match_array(['Snippet max file count exceeded'])
+ end
+ end
+
+ context 'when invalid attributes' do
+ let(:gist_file) { { file_name: '_Summary.md', file_content: nil } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Content can't be blank")
+ end
+ end
+
+ context 'when repository cloning fails' do
+ it 'returns error' do
+ expect_next_instance_of(Repository) do |repository|
+ expect(repository).to receive(:fetch_as_mirror).and_raise(Gitlab::Shell::Error)
+ expect(repository).to receive(:remove)
+ end
+
+ expect { subject }.to raise_error(Gitlab::Shell::Error)
+ expect(user.snippets.count).to eq(0)
+ end
+ end
+
+ context 'when url is invalid' do
+ let(:url) { 'invalid' }
+
+ context 'when local network is allowed' do
+ before do
+ allow(::Gitlab::CurrentSettings)
+ .to receive(:allow_local_requests_from_web_hooks_and_services?).and_return(true)
+ end
+
+ it 'raises error' do
+ expect(Gitlab::UrlBlocker)
+ .to receive(:validate!)
+ .with(url, ports: [80, 443], schemes: %w[http https git],
+ allow_localhost: true, allow_local_network: true)
+ .and_raise(Gitlab::UrlBlocker::BlockedUrlError)
+
+ expect { subject }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError)
+ end
+ end
+
+ context 'when local network is not allowed' do
+ before do
+ allow(::Gitlab::CurrentSettings)
+ .to receive(:allow_local_requests_from_web_hooks_and_services?).and_return(false)
+ end
+
+ it 'raises error' do
+ expect(Gitlab::UrlBlocker)
+ .to receive(:validate!)
+ .with(url, ports: [80, 443], schemes: %w[http https git],
+ allow_localhost: false, allow_local_network: false)
+ .and_raise(Gitlab::UrlBlocker::BlockedUrlError)
+
+ expect { subject }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_gists_import/importer/gists_importer_spec.rb b/spec/lib/gitlab/github_gists_import/importer/gists_importer_spec.rb
new file mode 100644
index 00000000000..704999a99a9
--- /dev/null
+++ b/spec/lib/gitlab/github_gists_import/importer/gists_importer_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubGistsImport::Importer::GistsImporter, feature_category: :importer do
+ subject(:result) { described_class.new(user, token).execute }
+
+ let_it_be(:user) { create(:user) }
+ let(:client) { instance_double('Gitlab::GithubImport::Client', rate_limit_resets_in: 5) }
+ let(:token) { 'token' }
+ let(:page_counter) { instance_double('Gitlab::GithubImport::PageCounter', current: 1, set: true, expire!: true) }
+ let(:page) { instance_double('Gitlab::GithubImport::Client::Page', objects: [gist], number: 1) }
+ let(:url) { 'https://gist.github.com/foo/bar.git' }
+ let(:waiter) { Gitlab::JobWaiter.new(0, 'some-job-key') }
+
+ let(:gist) do
+ {
+ id: '055b70',
+ git_pull_url: url,
+ files: {
+ 'random.txt': {
+ filename: 'random.txt',
+ type: 'text/plain',
+ language: 'Text',
+ raw_url: 'https://gist.githubusercontent.com/user_name/055b70/raw/66a7be0d/random.txt',
+ size: 166903
+ }
+ },
+ public: false,
+ created_at: '2022-09-06T11:38:18Z',
+ updated_at: '2022-09-06T11:38:18Z',
+ description: 'random text'
+ }
+ end
+
+ let(:gist_hash) do
+ {
+ id: '055b70',
+ import_url: url,
+ files: {
+ 'random.txt': {
+ filename: 'random.txt',
+ type: 'text/plain',
+ language: 'Text',
+ raw_url: 'https://gist.githubusercontent.com/user_name/055b70/raw/66a7be0d/random.txt',
+ size: 166903
+ }
+ },
+ public: false,
+ created_at: '2022-09-06T11:38:18Z',
+ updated_at: '2022-09-06T11:38:18Z',
+ title: 'random text'
+ }
+ end
+
+ let(:gist_represent) { instance_double('Gitlab::GithubGistsImport::Representation::Gist', to_hash: gist_hash) }
+
+ describe '#execute' do
+ before do
+ allow(Gitlab::GithubImport::Client)
+ .to receive(:new)
+ .with(token, parallel: true)
+ .and_return(client)
+
+ allow(Gitlab::GithubImport::PageCounter)
+ .to receive(:new)
+ .with(user, :gists, 'github-gists-importer')
+ .and_return(page_counter)
+
+ allow(client)
+ .to receive(:each_page)
+ .with(:gists, nil, { page: 1 })
+ .and_yield(page)
+
+ allow(Gitlab::GithubGistsImport::Representation::Gist)
+ .to receive(:from_api_response)
+ .with(gist)
+ .and_return(gist_represent)
+
+ allow(Gitlab::JobWaiter)
+ .to receive(:new)
+ .and_return(waiter)
+ end
+
+ context 'when success' do
+ it 'spread parallel import' do
+ expect(Gitlab::GithubGistsImport::ImportGistWorker)
+ .to receive(:bulk_perform_in)
+ .with(
+ 1.second,
+ [[user.id, gist_hash, waiter.key]],
+ batch_delay: 1.minute,
+ batch_size: 1000
+ )
+
+ expect(result.waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(result.waiter.jobs_remaining).to eq(1)
+ end
+ end
+
+ context 'when failure' do
+ it 'returns an error' do
+ expect(Gitlab::GithubGistsImport::ImportGistWorker)
+ .to receive(:bulk_perform_in)
+ .and_raise(StandardError, 'Error Message')
+
+ expect(result.error).to be_an_instance_of(StandardError)
+ end
+ end
+
+ context 'when rate limit reached' do
+ it 'returns an error' do
+ expect(Gitlab::GithubGistsImport::ImportGistWorker)
+ .to receive(:bulk_perform_in)
+ .and_raise(Gitlab::GithubImport::RateLimitError)
+
+ expect(result.error).to be_an_instance_of(Gitlab::GithubImport::RateLimitError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_gists_import/representation/gist_spec.rb b/spec/lib/gitlab/github_gists_import/representation/gist_spec.rb
new file mode 100644
index 00000000000..480aefb2c74
--- /dev/null
+++ b/spec/lib/gitlab/github_gists_import/representation/gist_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubGistsImport::Representation::Gist, feature_category: :importer do
+ shared_examples 'a Gist' do
+ it 'returns an instance of Gist' do
+ expect(gist).to be_an_instance_of(described_class)
+ end
+
+ context 'with Gist' do
+ it 'includes gist attributes' do
+ expect(gist).to have_attributes(
+ id: '1',
+ description: 'Gist title',
+ is_public: true,
+ files: { '_Summary.md': { filename: '_Summary.md', raw_url: 'https://some_url' } },
+ git_pull_url: 'https://gist.github.com/gistid.git'
+ )
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ let(:response) do
+ {
+ id: '1',
+ description: 'Gist title',
+ public: true,
+ created_at: '2022-04-26 18:30:53 UTC',
+ updated_at: '2022-04-26 18:30:53 UTC',
+ files: { '_Summary.md': { filename: '_Summary.md', raw_url: 'https://some_url' } },
+ git_pull_url: 'https://gist.github.com/gistid.git'
+ }
+ end
+
+ it_behaves_like 'a Gist' do
+ let(:gist) { described_class.from_api_response(response) }
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'a Gist' do
+ let(:hash) do
+ {
+ 'id' => '1',
+ 'description' => 'Gist title',
+ 'is_public' => true,
+ 'files' => { '_Summary.md': { filename: '_Summary.md', raw_url: 'https://some_url' } },
+ 'git_pull_url' => 'https://gist.github.com/gistid.git'
+ }
+ end
+
+ let(:gist) { described_class.from_json_hash(hash) }
+ end
+ end
+
+ describe '#truncated_title' do
+ it 'truncates the title to 255 characters' do
+ object = described_class.new(description: 'm' * 300)
+
+ expect(object.truncated_title.length).to eq(255)
+ end
+
+ it 'does not truncate the title if it is shorter than 255 characters' do
+ object = described_class.new(description: 'foo')
+
+ expect(object.truncated_title).to eq('foo')
+ end
+ end
+
+ describe '#github_identifiers' do
+ it 'returns a hash with needed identifiers' do
+ github_identifiers = { id: 1 }
+ gist = described_class.new(github_identifiers.merge(something_else: '_something_else_'))
+
+ expect(gist.github_identifiers).to eq(github_identifiers)
+ end
+ end
+
+ describe '#visibility_level' do
+ it 'returns 20 when public' do
+ visibility = { is_public: true }
+ gist = described_class.new(visibility.merge(something_else: '_something_else_'))
+
+ expect(gist.visibility_level).to eq(20)
+ end
+
+ it 'returns 0 when private' do
+ visibility = { is_public: false }
+ gist = described_class.new(visibility.merge(something_else: '_something_else_'))
+
+ expect(gist.visibility_level).to eq(0)
+ end
+ end
+
+ describe '#first_file' do
+ let(:http_response) { instance_double('HTTParty::Response', body: 'File content') }
+
+ before do
+ allow(Gitlab::HTTP).to receive(:try_get).and_return(http_response)
+ end
+
+ it 'returns a hash with needed identifiers' do
+ files = { files: { '_Summary.md': { filename: '_Summary.md', raw_url: 'https://some_url' } } }
+ gist = described_class.new(files.merge(something_else: '_something_else_'))
+
+ expect(gist.first_file).to eq(file_name: '_Summary.md', file_content: 'File content')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_gists_import/status_spec.rb b/spec/lib/gitlab/github_gists_import/status_spec.rb
new file mode 100644
index 00000000000..4cbbbd430eb
--- /dev/null
+++ b/spec/lib/gitlab/github_gists_import/status_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubGistsImport::Status, :clean_gitlab_redis_cache, feature_category: :importer do
+ subject(:import_status) { described_class.new(user.id) }
+
+ let_it_be(:user) { create(:user) }
+ let(:key) { "gitlab:github-gists-import:#{user.id}" }
+
+ describe '#start!' do
+ it 'expires the key' do
+ import_status.start!
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.get(key)).to eq('started')
+ end
+ end
+ end
+
+ describe '#fail!' do
+ it 'sets failed status' do
+ import_status.fail!
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.get(key)).to eq('failed')
+ end
+ end
+ end
+
+ describe '#finish!' do
+ it 'sets finished status' do
+ import_status.finish!
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.get(key)).to eq('finished')
+ end
+ end
+ end
+
+ describe '#started?' do
+ before do
+ Gitlab::Redis::SharedState.with { |redis| redis.set(key, 'started') }
+ end
+
+ it 'checks if status is started' do
+ expect(import_status.started?).to eq(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
index e170496ff7b..af31cb6c873 100644
--- a/spec/lib/gitlab/github_import/bulk_importing_spec.rb
+++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::BulkImporting do
+RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importer do
let(:project) { instance_double(Project, id: 1) }
let(:importer) { MyImporter.new(project, double) }
let(:importer_class) do
@@ -12,22 +12,33 @@ RSpec.describe Gitlab::GithubImport::BulkImporting do
def object_type
:object_type
end
+
+ def model
+ Label
+ end
end
end
+ let(:label) { instance_double('Label', invalid?: false) }
+
before do
stub_const 'MyImporter', importer_class
end
describe '#build_database_rows' do
- it 'returns an Array containing the rows to insert' do
+ it 'returns an Array containing the rows to insert and validation errors if object invalid' do
object = double(:object, title: 'Foo')
expect(importer)
- .to receive(:build)
+ .to receive(:build_attributes)
.with(object)
.and_return({ title: 'Foo' })
+ expect(Label)
+ .to receive(:new)
+ .with({ title: 'Foo' })
+ .and_return(label)
+
expect(importer)
.to receive(:already_imported?)
.with(object)
@@ -53,14 +64,17 @@ RSpec.describe Gitlab::GithubImport::BulkImporting do
enum = [[object, 1]].to_enum
- expect(importer.build_database_rows(enum)).to eq([{ title: 'Foo' }])
+ rows, errors = importer.build_database_rows(enum)
+
+ expect(rows).to match_array([{ title: 'Foo' }])
+ expect(errors).to be_empty
end
it 'does not import objects that have already been imported' do
object = double(:object, title: 'Foo')
expect(importer)
- .not_to receive(:build)
+ .not_to receive(:build_attributes)
expect(importer)
.to receive(:already_imported?)
@@ -87,14 +101,16 @@ RSpec.describe Gitlab::GithubImport::BulkImporting do
enum = [[object, 1]].to_enum
- expect(importer.build_database_rows(enum)).to be_empty
+ rows, errors = importer.build_database_rows(enum)
+
+ expect(rows).to be_empty
+ expect(errors).to be_empty
end
end
describe '#bulk_insert' do
it 'bulk inserts rows into the database' do
rows = [{ title: 'Foo' }] * 10
- model = double(:model, table_name: 'kittens')
expect(Gitlab::Import::Logger)
.to receive(:info)
@@ -119,14 +135,43 @@ RSpec.describe Gitlab::GithubImport::BulkImporting do
expect(ApplicationRecord)
.to receive(:legacy_bulk_insert)
.ordered
- .with('kittens', rows.first(5))
+ .with('labels', rows.first(5))
expect(ApplicationRecord)
.to receive(:legacy_bulk_insert)
.ordered
- .with('kittens', rows.last(5))
+ .with('labels', rows.last(5))
+
+ importer.bulk_insert(rows, batch_size: 5)
+ end
+ end
+
+ describe '#bulk_insert_failures', :timecop do
+ let(:import_failures) { instance_double('ImportFailure::ActiveRecord_Associations_CollectionProxy') }
+ let(:label) { Label.new(title: 'invalid,title') }
+ let(:validation_errors) { ActiveModel::Errors.new(label) }
+ let(:formatted_errors) do
+ [{
+ source: 'MyImporter',
+ exception_class: 'ActiveRecord::RecordInvalid',
+ exception_message: 'Title invalid',
+ correlation_id_value: 'cid',
+ retry_count: nil,
+ created_at: Time.zone.now
+ }]
+ end
- importer.bulk_insert(model, rows, batch_size: 5)
+ it 'bulk inserts validation errors into import_failures' do
+ error = ActiveModel::Errors.new(label)
+ error.add(:base, 'Title invalid')
+
+ freeze_time do
+ expect(project).to receive(:import_failures).and_return(import_failures)
+ expect(import_failures).to receive(:insert_all).with(formatted_errors)
+ expect(Labkit::Correlation::CorrelationId).to receive(:current_or_new_id).and_return('cid')
+
+ importer.bulk_insert_failures([error])
+ end
end
end
end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 95f7933fbc5..526a8721ff3 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -579,13 +579,69 @@ RSpec.describe Gitlab::GithubImport::Client do
allow(client.octokit).to receive(:user).and_return(user)
end
- describe '#search_repos_by_name' do
+ describe '#search_repos_by_name_graphql' do
+ let(:expected_query) { 'test in:name is:public,private user:user repo:repo1 repo:repo2 org:org1 org:org2' }
+ let(:expected_graphql_params) { "type: REPOSITORY, query: \"#{expected_query}\"" }
+ let(:expected_graphql) do
+ <<-TEXT
+ {
+ search(#{expected_graphql_params}) {
+ nodes {
+ __typename
+ ... on Repository {
+ id: databaseId
+ name
+ full_name: nameWithOwner
+ owner { login }
+ }
+ }
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
+ }
+ }
+ TEXT
+ end
+
it 'searches for repositories based on name' do
- expected_search_query = 'test in:name is:public,private user:user repo:repo1 repo:repo2 org:org1 org:org2'
+ expect(client.octokit).to receive(:post).with(
+ '/graphql', { query: expected_graphql }.to_json
+ )
- expect(client.octokit).to receive(:search_repositories).with(expected_search_query, {})
+ client.search_repos_by_name_graphql('test')
+ end
- client.search_repos_by_name('test')
+ context 'when pagination options present' do
+ context 'with "first" option' do
+ let(:expected_graphql_params) do
+ "type: REPOSITORY, query: \"#{expected_query}\", first: 25"
+ end
+
+ it 'searches for repositories via expected query' do
+ expect(client.octokit).to receive(:post).with(
+ '/graphql', { query: expected_graphql }.to_json
+ )
+
+ client.search_repos_by_name_graphql('test', { first: 25 })
+ end
+ end
+
+ context 'with "after" option' do
+ let(:expected_graphql_params) do
+ "type: REPOSITORY, query: \"#{expected_query}\", after: \"Y3Vyc29yOjE=\""
+ end
+
+ it 'searches for repositories via expected query' do
+ expect(client.octokit).to receive(:post).with(
+ '/graphql', { query: expected_graphql }.to_json
+ )
+
+ client.search_repos_by_name_graphql('test', { after: 'Y3Vyc29yOjE=' })
+ end
+ end
end
context 'when Faraday error received from octokit', :aggregate_failures do
@@ -593,41 +649,62 @@ RSpec.describe Gitlab::GithubImport::Client do
let(:info_params) { { 'error.class': error_class } }
it 'retries on error and succeeds' do
- allow_retry(:search_repositories)
+ allow_retry(:post)
expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once
- expect(client.search_repos_by_name('test')).to eq({})
+ expect(client.search_repos_by_name_graphql('test')).to eq({})
end
it 'retries and does not succeed' do
- allow(client.octokit).to receive(:search_repositories).and_raise(error_class, 'execution expired')
+ allow(client.octokit)
+ .to receive(:post)
+ .with('/graphql', { query: expected_graphql }.to_json)
+ .and_raise(error_class, 'execution expired')
- expect { client.search_repos_by_name('test') }.to raise_error(error_class, 'execution expired')
+ expect { client.search_repos_by_name_graphql('test') }.to raise_error(error_class, 'execution expired')
end
end
end
- describe '#search_query' do
- it 'returns base search query' do
- result = client.search_query(str: 'test', type: :test, include_collaborations: false, include_orgs: false)
+ describe '#search_repos_by_name' do
+ let(:expected_query) { 'test in:name is:public,private user:user repo:repo1 repo:repo2 org:org1 org:org2' }
- expect(result).to eq('test in:test is:public,private user:user')
+ it 'searches for repositories based on name' do
+ expect(client.octokit).to receive(:search_repositories).with(expected_query, {})
+
+ client.search_repos_by_name('test')
end
- context 'when include_collaborations is true' do
- it 'returns search query including users' do
- result = client.search_query(str: 'test', type: :test, include_collaborations: true, include_orgs: false)
+ context 'when pagination options present' do
+ it 'searches for repositories via expected query' do
+ expect(client.octokit).to receive(:search_repositories).with(
+ expected_query, { page: 2, per_page: 25 }
+ )
- expect(result).to eq('test in:test is:public,private user:user repo:repo1 repo:repo2')
+ client.search_repos_by_name('test', { page: 2, per_page: 25 })
end
end
- context 'when include_orgs is true' do
- it 'returns search query including users' do
- result = client.search_query(str: 'test', type: :test, include_collaborations: false, include_orgs: true)
+ context 'when Faraday error received from octokit', :aggregate_failures do
+ let(:error_class) { described_class::CLIENT_CONNECTION_ERROR }
+ let(:info_params) { { 'error.class': error_class } }
+
+ it 'retries on error and succeeds' do
+ allow_retry(:search_repositories)
+
+ expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once
+
+ expect(client.search_repos_by_name('test')).to eq({})
+ end
+
+ it 'retries and does not succeed' do
+ allow(client.octokit)
+ .to receive(:search_repositories)
+ .with(expected_query, {})
+ .and_raise(error_class, 'execution expired')
- expect(result).to eq('test in:test is:public,private user:user org:org1 org:org2')
+ expect { client.search_repos_by_name('test') }.to raise_error(error_class, 'execution expired')
end
end
end
diff --git a/spec/lib/gitlab/github_import/clients/proxy_spec.rb b/spec/lib/gitlab/github_import/clients/proxy_spec.rb
new file mode 100644
index 00000000000..9fef57f2a38
--- /dev/null
+++ b/spec/lib/gitlab/github_import/clients/proxy_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Clients::Proxy, :manage, feature_category: :import do
+ subject(:client) { described_class.new(access_token, client_options) }
+
+ let(:access_token) { 'test_token' }
+ let(:client_options) { { foo: :bar } }
+
+ describe '#repos' do
+ let(:search_text) { 'search text' }
+ let(:pagination_options) { { limit: 10 } }
+
+ context 'when remove_legacy_github_client FF is enabled' do
+ let(:client_stub) { instance_double(Gitlab::GithubImport::Client) }
+
+ context 'with github_client_fetch_repos_via_graphql FF enabled' do
+ let(:client_response) do
+ {
+ data: {
+ search: {
+ nodes: [{ name: 'foo' }, { name: 'bar' }],
+ pageInfo: { startCursor: 'foo', endCursor: 'bar' }
+ }
+ }
+ }
+ end
+
+ it 'fetches repos with Gitlab::GithubImport::Client (GraphQL API)' do
+ expect(Gitlab::GithubImport::Client)
+ .to receive(:new).with(access_token).and_return(client_stub)
+ expect(client_stub)
+ .to receive(:search_repos_by_name_graphql)
+ .with(search_text, pagination_options).and_return(client_response)
+
+ expect(client.repos(search_text, pagination_options)).to eq(
+ {
+ repos: [{ name: 'foo' }, { name: 'bar' }],
+ page_info: { startCursor: 'foo', endCursor: 'bar' }
+ }
+ )
+ end
+ end
+
+ context 'with github_client_fetch_repos_via_graphql FF disabled' do
+ let(:client_response) do
+ { items: [{ name: 'foo' }, { name: 'bar' }] }
+ end
+
+ before do
+ stub_feature_flags(github_client_fetch_repos_via_graphql: false)
+ end
+
+ it 'fetches repos with Gitlab::GithubImport::Client (REST API)' do
+ expect(Gitlab::GithubImport::Client)
+ .to receive(:new).with(access_token).and_return(client_stub)
+ expect(client_stub)
+ .to receive(:search_repos_by_name)
+ .with(search_text, pagination_options).and_return(client_response)
+
+ expect(client.repos(search_text, pagination_options)).to eq(
+ { repos: [{ name: 'foo' }, { name: 'bar' }] }
+ )
+ end
+ end
+ end
+
+ context 'when remove_legacy_github_client FF is disabled' do
+ let(:client_stub) { instance_double(Gitlab::LegacyGithubImport::Client) }
+ let(:search_text) { nil }
+
+ before do
+ stub_feature_flags(remove_legacy_github_client: false)
+ end
+
+ it 'fetches repos with Gitlab::LegacyGithubImport::Client' do
+ expect(Gitlab::LegacyGithubImport::Client)
+ .to receive(:new).with(access_token, client_options).and_return(client_stub)
+ expect(client_stub).to receive(:repos)
+ .and_return([{ name: 'foo' }, { name: 'bar' }])
+
+ expect(client.repos(search_text, pagination_options))
+ .to eq({ repos: [{ name: 'foo' }, { name: 'bar' }] })
+ end
+
+ context 'with filter params' do
+ let(:search_text) { 'fo' }
+
+ it 'fetches repos with Gitlab::LegacyGithubImport::Client' do
+ expect(Gitlab::LegacyGithubImport::Client)
+ .to receive(:new).with(access_token, client_options).and_return(client_stub)
+ expect(client_stub).to receive(:repos)
+ .and_return([{ name: 'FOO' }, { name: 'bAr' }])
+
+ expect(client.repos(search_text, pagination_options))
+ .to eq({ repos: [{ name: 'FOO' }] })
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
index 8eeb2332131..73ba49bf4ed 100644
--- a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
@@ -35,7 +35,8 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_fail
end_line: end_line,
github_id: 1,
diff_hunk: diff_hunk,
- side: 'RIGHT'
+ side: 'RIGHT',
+ discussion_id: discussion_id
)
end
@@ -114,10 +115,6 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_fail
.to receive(:database_id)
.and_return(merge_request.id)
end
-
- expect(Discussion)
- .to receive(:discussion_id)
- .and_return(discussion_id)
end
it_behaves_like 'diff notes without suggestion'
@@ -218,6 +215,16 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_fail
end
end
end
+
+ context 'when diff note is invalid' do
+ it 'fails validation' do
+ stub_user_finder(user.id, true)
+
+ expect(note_representation).to receive(:line_code).and_return(nil)
+
+ expect { subject.execute }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
end
end
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 308b8185589..4a5525c250e 100644
--- a/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
@@ -90,9 +90,13 @@ RSpec.describe Gitlab::GithubImport::Importer::IssuesImporter do
.to receive(:each_object_to_import)
.and_yield(github_issue)
- expect(Gitlab::GithubImport::ImportIssueWorker).to receive(:bulk_perform_in).with(1.second, [
- [project.id, an_instance_of(Hash), an_instance_of(String)]
- ], batch_size: 1000, batch_delay: 1.minute)
+ expect(Gitlab::GithubImport::ImportIssueWorker)
+ .to receive(:bulk_perform_in)
+ .with(1.second,
+ [[project.id, an_instance_of(Hash), an_instance_of(String)]],
+ batch_size: 1000,
+ batch_delay: 1.minute
+ )
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
index e68849755b2..e005d8eda84 100644
--- a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
@@ -36,26 +36,11 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelLinksImporter do
expect(importer)
.to receive(:find_target_id)
- .and_return(1)
+ .and_return(4)
- freeze_time do
- expect(ApplicationRecord)
- .to receive(:legacy_bulk_insert)
- .with(
- LabelLink.table_name,
- [
- {
- label_id: 2,
- target_id: 1,
- target_type: Issue,
- created_at: Time.zone.now,
- updated_at: Time.zone.now
- }
- ]
- )
+ expect(LabelLink).to receive(:bulk_insert!)
- importer.create_labels
- end
+ importer.create_labels
end
it 'does not insert label links for non-existing labels' do
@@ -64,9 +49,9 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelLinksImporter do
.with('bug')
.and_return(nil)
- expect(ApplicationRecord)
- .to receive(:legacy_bulk_insert)
- .with(LabelLink.table_name, [])
+ expect(LabelLink)
+ .to receive(:bulk_insert!)
+ .with([])
importer.create_labels
end
diff --git a/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
index 81d534c566f..ad9ef4afddd 100644
--- a/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::LabelsImporter, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::GithubImport::Importer::LabelsImporter, :clean_gitlab_redis_cache, feature_category: :importer do
let(:project) { create(:project, import_source: 'foo/bar') }
let(:client) { double(:client) }
let(:importer) { described_class.new(project, client) }
@@ -11,40 +11,58 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelsImporter, :clean_gitlab_red
it 'imports the labels in bulk' do
label_hash = { title: 'bug', color: '#fffaaa' }
- expect(importer)
- .to receive(:build_labels)
- .and_return([label_hash])
-
- expect(importer)
- .to receive(:bulk_insert)
- .with(Label, [label_hash])
-
- expect(importer)
- .to receive(:build_labels_cache)
+ expect(importer).to receive(:build_labels).and_return([[label_hash], []])
+ expect(importer).to receive(:bulk_insert).with([label_hash])
+ expect(importer).to receive(:build_labels_cache)
importer.execute
end
end
describe '#build_labels' do
- it 'returns an Array containnig label rows' do
+ it 'returns an Array containing label rows' do
label = { name: 'bug', color: 'ffffff' }
expect(importer).to receive(:each_label).and_return([label])
- rows = importer.build_labels
+ rows, errors = importer.build_labels
expect(rows.length).to eq(1)
expect(rows[0][:title]).to eq('bug')
+ expect(errors).to be_blank
end
- it 'does not create labels that already exist' do
+ it 'does not build labels that already exist' do
create(:label, project: project, title: 'bug')
label = { name: 'bug', color: 'ffffff' }
expect(importer).to receive(:each_label).and_return([label])
- expect(importer.build_labels).to be_empty
+
+ rows, errors = importer.build_labels
+
+ expect(rows).to be_empty
+ expect(errors).to be_empty
+ end
+
+ it 'does not build labels that are invalid' do
+ label = { id: 1, name: 'bug,bug', color: 'ffffff' }
+
+ expect(importer).to receive(:each_label).and_return([label])
+ expect(Gitlab::Import::Logger).to receive(:error)
+ .with(
+ import_type: :github,
+ project_id: project.id,
+ importer: described_class.name,
+ message: ['Title is invalid'],
+ github_identifier: 1
+ )
+
+ rows, errors = importer.build_labels
+
+ expect(rows).to be_empty
+ expect(errors.length).to eq(1)
+ expect(errors[0].full_messages).to match_array(['Title is invalid'])
end
end
@@ -58,9 +76,9 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelsImporter, :clean_gitlab_red
end
end
- describe '#build' do
+ describe '#build_attributes' do
let(:label_hash) do
- importer.build({ name: 'bug', color: 'ffffff' })
+ importer.build_attributes({ name: 'bug', color: 'ffffff' })
end
it 'returns the attributes of the label as a Hash' do
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 99536588718..678aa705b6c 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
@@ -58,7 +58,7 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service|
expect(service)
- .to receive(:execute)
+ .to receive(:each_list_item)
.and_raise(exception)
end
@@ -79,7 +79,7 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service|
expect(service)
- .to receive(:execute)
+ .to receive(:each_list_item)
.and_raise(exception)
end
@@ -94,7 +94,7 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
lfs_object_importer = double(:lfs_object_importer)
expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service|
- expect(service).to receive(:execute).and_return([lfs_download_object])
+ expect(service).to receive(:each_list_item).and_yield(lfs_download_object)
end
expect(Gitlab::GithubImport::Importer::LfsObjectImporter)
@@ -115,7 +115,7 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
importer = described_class.new(project, client)
expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service|
- expect(service).to receive(:execute).and_return([lfs_download_object])
+ expect(service).to receive(:each_list_item).and_yield(lfs_download_object)
end
expect(Gitlab::GithubImport::ImportLfsObjectWorker).to receive(:bulk_perform_in)
diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
index 04d76bd1f06..8667729d79b 100644
--- a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis_cache,
+ feature_category: :importer do
let(:project) { create(:project, import_source: 'foo/bar') }
let(:client) { double(:client) }
let(:importer) { described_class.new(project, client) }
@@ -38,41 +39,61 @@ RSpec.describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab
it 'imports the milestones in bulk' do
milestone_hash = { number: 1, title: '1.0' }
- expect(importer)
- .to receive(:build_milestones)
- .and_return([milestone_hash])
-
- expect(importer)
- .to receive(:bulk_insert)
- .with(Milestone, [milestone_hash])
-
- expect(importer)
- .to receive(:build_milestones_cache)
+ expect(importer).to receive(:build_milestones).and_return([[milestone_hash], []])
+ expect(importer).to receive(:bulk_insert).with([milestone_hash])
+ expect(importer).to receive(:build_milestones_cache)
importer.execute
end
end
describe '#build_milestones' do
- it 'returns an Array containnig milestone rows' do
+ it 'returns an Array containing milestone rows' do
expect(importer)
.to receive(:each_milestone)
.and_return([milestone])
- rows = importer.build_milestones
+ rows, errors = importer.build_milestones
expect(rows.length).to eq(1)
expect(rows[0][:title]).to eq('1.0')
+ expect(errors).to be_empty
end
- it 'does not create milestones that already exist' do
+ it 'does not build milestones that already exist' do
create(:milestone, project: project, title: '1.0', iid: 1)
expect(importer)
.to receive(:each_milestone)
.and_return([milestone])
- expect(importer.build_milestones).to be_empty
+ rows, errors = importer.build_milestones
+
+ expect(rows).to be_empty
+ expect(errors).to be_empty
+ end
+
+ it 'does not build milestones that are invalid' do
+ milestone = { id: 1, title: nil }
+
+ expect(importer)
+ .to receive(:each_milestone)
+ .and_return([milestone])
+
+ expect(Gitlab::Import::Logger).to receive(:error)
+ .with(
+ import_type: :github,
+ project_id: project.id,
+ importer: described_class.name,
+ message: ["Title can't be blank"],
+ github_identifier: 1
+ )
+
+ rows, errors = importer.build_milestones
+
+ expect(rows).to be_empty
+ expect(errors.length).to eq(1)
+ expect(errors[0].full_messages).to match_array(["Title can't be blank"])
end
end
@@ -86,9 +107,9 @@ RSpec.describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab
end
end
- describe '#build' do
- let(:milestone_hash) { importer.build(milestone) }
- let(:milestone_hash2) { importer.build(milestone2) }
+ describe '#build_attributes' do
+ let(:milestone_hash) { importer.build_attributes(milestone) }
+ let(:milestone_hash2) { importer.build_attributes(milestone2) }
it 'returns the attributes of the milestone as a Hash' do
expect(milestone_hash).to be_an_instance_of(Hash)
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 c60ecd85e92..5ac50578b6a 100644
--- a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
@@ -113,6 +113,19 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do
.to eq('There were an invalid char "" <= right here')
end
end
+
+ context 'when note is invalid' do
+ it 'fails validation' do
+ expect(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(github_note)
+ .and_return([user.id, true])
+
+ expect(github_note).to receive(:discussion_id).and_return('invalid')
+
+ expect { importer.execute }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
end
context 'when the noteable does not exist' do
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
index c7388314253..dd73b6879e0 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
@@ -202,6 +202,20 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla
importer.create_merge_request
end
end
+
+ context 'when merge request is invalid' do
+ before do
+ allow(pull_request).to receive(:formatted_source_branch).and_return(nil)
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(pull_request)
+ .and_return([project.creator_id, false])
+ end
+
+ it 'fails validation' do
+ expect { importer.create_merge_request }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
end
describe '#set_merge_request_assignees' do
@@ -292,6 +306,16 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla
expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey
expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
end
+
+ it 'ignores Git PreReceive errors when creating a branch' do
+ expect(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::PreReceiveError)
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original
+
+ mr = insert_git_data
+
+ expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey
+ expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
+ end
end
it 'creates a merge request diff and sets it as the latest' do
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb
index f3a9bbac785..01d706beea2 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb
@@ -22,6 +22,23 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestMergedByImporter, :cle
subject { described_class.new(pull_request, project, client_double) }
+ shared_examples 'adds a note referencing the merger user' do
+ it 'adds a note referencing the merger user' do
+ expect { subject.execute }
+ .to change(Note, :count).by(1)
+ .and not_change(merge_request, :updated_at)
+
+ metrics = merge_request.metrics.reload
+ expect(metrics.merged_by).to be_nil
+ expect(metrics.merged_at).to eq(merged_at)
+
+ last_note = merge_request.notes.last
+ expect(last_note.created_at).to eq(merged_at)
+ expect(last_note.author).to eq(project.creator)
+ expect(last_note.note).to eq("*Merged by: merger at #{merged_at}*")
+ end
+ end
+
context 'when the merger user can be mapped' do
it 'assigns the merged by user when mapped' do
merge_user = create(:user, email: 'merger@email.com')
@@ -35,19 +52,14 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestMergedByImporter, :cle
end
context 'when the merger user cannot be mapped to a gitlab user' do
- it 'adds a note referencing the merger user' do
- expect { subject.execute }
- .to change(Note, :count).by(1)
- .and not_change(merge_request, :updated_at)
+ it_behaves_like 'adds a note referencing the merger user'
- metrics = merge_request.metrics.reload
- expect(metrics.merged_by).to be_nil
- expect(metrics.merged_at).to eq(merged_at)
+ context 'when original user cannot be found on github' do
+ before do
+ allow(client_double).to receive(:user).and_raise(Octokit::NotFound)
+ end
- last_note = merge_request.notes.last
- expect(last_note.note).to eq("*Merged by: merger at 2017-01-01 12:00:00 UTC*")
- expect(last_note.created_at).to eq(merged_at)
- expect(last_note.author).to eq(project.creator)
+ it_behaves_like 'adds a note referencing the merger user'
end
end
@@ -64,9 +76,9 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestMergedByImporter, :cle
expect(metrics.merged_at).to eq(merged_at)
last_note = merge_request.notes.last
- expect(last_note.note).to eq("*Merged by: ghost at 2017-01-01 12:00:00 UTC*")
expect(last_note.created_at).to eq(merged_at)
expect(last_note.author).to eq(project.creator)
+ expect(last_note.note).to eq("*Merged by: ghost at #{merged_at}*")
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb
index 49794eceb5a..2e1a3c496cc 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb
@@ -208,6 +208,23 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean
end
end
+ context 'when original author cannot be found on github' do
+ before do
+ allow(client_double).to receive(:user).and_raise(Octokit::NotFound)
+ end
+
+ let(:review) { create_review(type: 'APPROVED', note: '') }
+
+ it 'creates a note for the review with the author username' do
+ expect { subject.execute }
+ .to change(Note, :count).by(1)
+ last_note = merge_request.notes.last
+ expect(last_note.note).to eq("*Created by: author*\n\n**Review:** Approved")
+ expect(last_note.author).to eq(project.creator)
+ expect(last_note.created_at).to eq(submitted_at)
+ end
+ end
+
context 'when the submitted_at is not provided' do
let(:review) { create_review(type: 'APPROVED', note: '', submitted_at: nil) }
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 6c7fc4d5b15..3bf1976ee10 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
@@ -109,7 +109,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor
expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker)
.to receive(:bulk_perform_in).with(
1.second,
- expected_worker_payload,
+ match_array(expected_worker_payload),
batch_size: 1000,
batch_delay: 1.minute
)
diff --git a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
index 84d639a09ef..ccbe5b5fc50 100644
--- a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::ReleasesImporter do
+RSpec.describe Gitlab::GithubImport::Importer::ReleasesImporter, feature_category: :importer do
let(:project) { create(:project) }
let(:client) { double(:client) }
let(:importer) { described_class.new(project, client) }
@@ -48,8 +48,8 @@ RSpec.describe Gitlab::GithubImport::Importer::ReleasesImporter do
released_at: released_at
}
- expect(importer).to receive(:build_releases).and_return([release_hash])
- expect(importer).to receive(:bulk_insert).with(Release, [release_hash])
+ expect(importer).to receive(:build_releases).and_return([[release_hash], []])
+ expect(importer).to receive(:bulk_insert).with([release_hash])
importer.execute
end
@@ -86,24 +86,29 @@ RSpec.describe Gitlab::GithubImport::Importer::ReleasesImporter do
it 'returns an Array containing release rows' do
expect(importer).to receive(:each_release).and_return([github_release])
- rows = importer.build_releases
+ rows, errors = importer.build_releases
expect(rows.length).to eq(1)
expect(rows[0][:tag]).to eq('1.0')
+ expect(errors).to be_empty
end
it 'does not create releases that already exist' do
create(:release, project: project, tag: '1.0', description: '1.0')
expect(importer).to receive(:each_release).and_return([github_release])
- expect(importer.build_releases).to be_empty
+
+ rows, errors = importer.build_releases
+
+ expect(rows).to be_empty
+ expect(errors).to be_empty
end
it 'uses a default release description if none is provided' do
github_release[:body] = nil
expect(importer).to receive(:each_release).and_return([github_release])
- release = importer.build_releases.first
+ release, _ = importer.build_releases.first
expect(release[:description]).to eq('Release for tag 1.0')
end
@@ -115,20 +120,36 @@ RSpec.describe Gitlab::GithubImport::Importer::ReleasesImporter do
}
expect(importer).to receive(:each_release).and_return([null_tag_release])
- expect(importer.build_releases).to be_empty
+
+ rows, errors = importer.build_releases
+
+ expect(rows).to be_empty
+ expect(errors).to be_empty
end
it 'does not create duplicate release tags' do
expect(importer).to receive(:each_release).and_return([github_release, github_release])
- releases = importer.build_releases
+ releases, _ = importer.build_releases
expect(releases.length).to eq(1)
expect(releases[0][:description]).to eq('This is my release')
end
+
+ it 'does not create invalid release' do
+ github_release[:body] = SecureRandom.alphanumeric(Gitlab::Database::MAX_TEXT_SIZE_LIMIT + 1)
+
+ expect(importer).to receive(:each_release).and_return([github_release])
+
+ releases, errors = importer.build_releases
+
+ expect(releases).to be_empty
+ expect(errors.length).to eq(1)
+ expect(errors[0].full_messages).to match_array(['Description is too long (maximum is 1000000 characters)'])
+ end
end
- describe '#build' do
- let(:release_hash) { importer.build(github_release) }
+ describe '#build_attributes' do
+ let(:release_hash) { importer.build_attributes(github_release) }
context 'the returned Hash' do
before do
diff --git a/spec/lib/gitlab/github_import/markdown/attachment_spec.rb b/spec/lib/gitlab/github_import/markdown/attachment_spec.rb
index 5d29de34141..588a3076f59 100644
--- a/spec/lib/gitlab/github_import/markdown/attachment_spec.rb
+++ b/spec/lib/gitlab/github_import/markdown/attachment_spec.rb
@@ -35,6 +35,12 @@ RSpec.describe Gitlab::GithubImport::Markdown::Attachment do
it { expect(described_class.from_markdown(markdown_node)).to eq nil }
end
+
+ context 'when URL is blank' do
+ let(:url) { nil }
+
+ it { expect(described_class.from_markdown(markdown_node)).to eq nil }
+ end
end
context "when it's an image attachment" do
@@ -63,6 +69,12 @@ RSpec.describe Gitlab::GithubImport::Markdown::Attachment do
it { expect(described_class.from_markdown(markdown_node)).to eq nil }
end
+
+ context 'when URL is blank' do
+ let(:url) { nil }
+
+ it { expect(described_class.from_markdown(markdown_node)).to eq nil }
+ end
end
context "when it's an inline html node" do
@@ -80,6 +92,12 @@ RSpec.describe Gitlab::GithubImport::Markdown::Attachment do
expect(attachment.name).to eq name
expect(attachment.url).to eq url
end
+
+ context 'when image src is not present' do
+ let(:img) { "<img width=\"248\" alt=\"#{name}\">" }
+
+ it { expect(described_class.from_markdown(markdown_node)).to eq nil }
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/page_counter_spec.rb b/spec/lib/gitlab/github_import/page_counter_spec.rb
index 568bc8cbbef..511b19c00e5 100644
--- a/spec/lib/gitlab/github_import/page_counter_spec.rb
+++ b/spec/lib/gitlab/github_import/page_counter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::PageCounter, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::GithubImport::PageCounter, :clean_gitlab_redis_cache, feature_category: :importer do
let(:project) { double(:project, id: 1) }
let(:counter) { described_class.new(project, :issues) }
@@ -16,6 +16,16 @@ RSpec.describe Gitlab::GithubImport::PageCounter, :clean_gitlab_redis_cache do
expect(described_class.new(project, :issues).current).to eq(2)
end
+
+ context 'when gists import' do
+ let(:user) { instance_double('User', id: 2) }
+
+ it 'uses gists specific key' do
+ result = described_class.new(user, :gists, 'github-gists-importer')
+
+ expect(result.cache_key).to eq('github-gists-importer/page-counter/2/gists')
+ end
+ end
end
describe '#set' do
diff --git a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
index a656cd0d056..56fabe854f9 100644
--- a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
@@ -128,64 +128,6 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_red
end
end
- describe '#discussion_id' do
- before do
- note.project = project
- note.merge_request = merge_request
- end
-
- context 'when the note is a reply to a discussion' do
- it 'uses the cached value as the discussion_id only when responding an existing discussion' do
- expect(Discussion)
- .to receive(:discussion_id)
- .and_return('FIRST_DISCUSSION_ID', 'SECOND_DISCUSSION_ID')
-
- # Creates the first discussion id and caches its value
- expect(note.discussion_id)
- .to eq('FIRST_DISCUSSION_ID')
-
- reply_note = described_class.from_json_hash(
- 'note_id' => note.note_id + 1,
- 'in_reply_to_id' => note.note_id
- )
- reply_note.project = project
- reply_note.merge_request = merge_request
-
- # Reading from the cached value
- expect(reply_note.discussion_id)
- .to eq('FIRST_DISCUSSION_ID')
-
- new_discussion_note = described_class.from_json_hash(
- 'note_id' => note.note_id + 2,
- 'in_reply_to_id' => nil
- )
- new_discussion_note.project = project
- new_discussion_note.merge_request = merge_request
-
- # Because it's a new discussion, it must not use the cached value
- expect(new_discussion_note.discussion_id)
- .to eq('SECOND_DISCUSSION_ID')
- end
-
- context 'when cached value does not exist' do
- it 'falls back to generating a new discussion_id' do
- expect(Discussion)
- .to receive(:discussion_id)
- .and_return('NEW_DISCUSSION_ID')
-
- reply_note = described_class.from_json_hash(
- 'note_id' => note.note_id + 1,
- 'in_reply_to_id' => note.note_id
- )
- reply_note.project = project
- reply_note.merge_request = merge_request
-
- expect(reply_note.discussion_id).to eq('NEW_DISCUSSION_ID')
- end
- end
- end
- end
-
describe '#github_identifiers' do
it 'returns a hash with needed identifiers' do
expect(note.github_identifiers).to eq(
@@ -273,27 +215,40 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_red
end
describe '.from_api_response' do
- it_behaves_like 'a DiffNote representation' do
- let(:response) do
- {
- id: note_id,
- html_url: 'https://github.com/foo/bar/pull/42',
- path: 'README.md',
- commit_id: '123abc',
- original_commit_id: 'original123abc',
- side: side,
- user: user_data,
- diff_hunk: hunk,
- body: note_body,
- created_at: created_at,
- updated_at: updated_at,
- line: end_line,
- start_line: start_line,
- in_reply_to_id: in_reply_to_id
- }
- end
+ let(:response) do
+ {
+ id: note_id,
+ html_url: 'https://github.com/foo/bar/pull/42',
+ path: 'README.md',
+ commit_id: '123abc',
+ original_commit_id: 'original123abc',
+ side: side,
+ user: user_data,
+ diff_hunk: hunk,
+ body: note_body,
+ created_at: created_at,
+ updated_at: updated_at,
+ line: end_line,
+ start_line: start_line,
+ in_reply_to_id: in_reply_to_id
+ }
+ end
+
+ subject(:note) { described_class.from_api_response(response) }
+
+ it_behaves_like 'a DiffNote representation'
+
+ describe '#discussion_id' do
+ it 'finds or generates discussion_id value' do
+ discussion_id = 'discussion_id'
+ discussion_id_class = Gitlab::GithubImport::Representation::DiffNotes::DiscussionId
- subject(:note) { described_class.from_api_response(response) }
+ expect_next_instance_of(discussion_id_class, response) do |discussion_id_object|
+ expect(discussion_id_object).to receive(:find_or_generate).and_return(discussion_id)
+ end
+
+ expect(note.discussion_id).to eq(discussion_id)
+ end
end
end
@@ -302,6 +257,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_red
let(:hash) do
{
'note_id' => note_id,
+ 'html_url' => 'https://github.com/foo/bar/pull/42',
'noteable_type' => 'MergeRequest',
'noteable_id' => 42,
'file_path' => 'README.md',
@@ -315,7 +271,8 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_red
'updated_at' => updated_at.to_s,
'end_line' => end_line,
'start_line' => start_line,
- 'in_reply_to_id' => in_reply_to_id
+ 'in_reply_to_id' => in_reply_to_id,
+ 'discussion_id' => 'FIRST_DISCUSSION_ID'
}
end
diff --git a/spec/lib/gitlab/github_import/representation/diff_notes/discussion_id_spec.rb b/spec/lib/gitlab/github_import/representation/diff_notes/discussion_id_spec.rb
new file mode 100644
index 00000000000..64a16516e07
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/diff_notes/discussion_id_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Representation::DiffNotes::DiscussionId, :clean_gitlab_redis_cache,
+ feature_category: :importers do
+ describe '#discussion_id' do
+ let(:hunk) do
+ '@@ -1 +1 @@
+ -Hello
+ +Hello world'
+ end
+
+ let(:note_id) { 1 }
+ let(:html_url) { 'https://github.com/foo/project_name/pull/42' }
+ let(:note) do
+ {
+ id: note_id,
+ html_url: html_url,
+ path: 'README.md',
+ commit_id: '123abc',
+ original_commit_id: 'original123abc',
+ side: 'RIGHT',
+ user: { id: 4, login: 'alice' },
+ diff_hunk: hunk,
+ body: 'Hello world',
+ created_at: Time.new(2017, 1, 1, 12, 10).utc,
+ updated_at: Time.new(2017, 1, 1, 12, 15).utc,
+ line: 23,
+ start_line: nil,
+ in_reply_to_id: nil
+ }
+ end
+
+ context 'when the note is not a reply to a discussion' do
+ subject(:discussion_id) { described_class.new(note).find_or_generate }
+
+ it 'generates and caches new discussion_id' do
+ expect(Discussion)
+ .to receive(:discussion_id)
+ .and_return('FIRST_DISCUSSION_ID')
+
+ expect(Gitlab::Cache::Import::Caching).to receive(:write).with(
+ "github-importer/discussion-id-map/project_name/42/#{note_id}",
+ 'FIRST_DISCUSSION_ID'
+ ).and_return('FIRST_DISCUSSION_ID')
+
+ expect(discussion_id).to eq('FIRST_DISCUSSION_ID')
+ end
+ end
+
+ context 'when the note is a reply to a discussion' do
+ let(:reply_note) do
+ {
+ note_id: note_id + 1,
+ in_reply_to_id: note_id,
+ html_url: html_url
+ }
+ end
+
+ subject(:discussion_id) { described_class.new(reply_note).find_or_generate }
+
+ it 'uses the cached value as the discussion_id' do
+ expect(Discussion)
+ .to receive(:discussion_id)
+ .and_return('FIRST_DISCUSSION_ID')
+
+ described_class.new(note).find_or_generate
+
+ expect(discussion_id).to eq('FIRST_DISCUSSION_ID')
+ end
+
+ context 'when cached value does not exist' do
+ it 'falls back to generating a new discussion_id' do
+ expect(Discussion)
+ .to receive(:discussion_id)
+ .and_return('NEW_DISCUSSION_ID')
+
+ expect(discussion_id).to eq('NEW_DISCUSSION_ID')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb
index 5a1fcc5e2dc..6e8997d51c3 100644
--- a/spec/lib/gitlab/gon_helper_spec.rb
+++ b/spec/lib/gitlab/gon_helper_spec.rb
@@ -40,11 +40,11 @@ RSpec.describe Gitlab::GonHelper do
end
end
- describe 'sentry configuration' do
+ context 'when sentry is configured' do
let(:clientside_dsn) { 'https://xxx@sentry.example.com/1' }
let(:environment) { 'staging' }
- describe 'sentry integration' do
+ context 'with legacy sentry configuration' do
before do
stub_config(sentry: { enabled: true, clientside_dsn: clientside_dsn, environment: environment })
end
@@ -57,7 +57,7 @@ RSpec.describe Gitlab::GonHelper do
end
end
- describe 'new sentry integration' do
+ context 'with sentry settings' do
before do
stub_application_setting(sentry_enabled: true)
stub_application_setting(sentry_clientside_dsn: clientside_dsn)
@@ -104,6 +104,7 @@ RSpec.describe Gitlab::GonHelper do
thing = stub_feature_flag_gate('thing')
stub_feature_flags(my_feature_flag: thing)
+ stub_feature_flag_definition(:my_feature_flag)
allow(helper)
.to receive(:gon)
diff --git a/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb b/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb
index 5858986dfc8..afcfb063af3 100644
--- a/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb
+++ b/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb
@@ -36,6 +36,15 @@ RSpec.describe Gitlab::Graphql::Limit::FieldCallCount do
expect(resolve_value).to be_an_instance_of(Gitlab::Graphql::Errors::LimitError)
end
+ it 'does not return an error when the field is called multiple times in separte queries' do
+ query_1 = GraphQL::Query.new(GitlabSchema)
+ query_2 = GraphQL::Query.new(GitlabSchema)
+
+ resolve_field(field, { value: 'foo' }, object_type: owner, query: query_1)
+
+ expect { resolve_field(field, { value: 'foo' }, object_type: owner, query: query_2) }.not_to raise_error
+ end
+
context 'when limit is not specified' do
let(:field) do
::Types::BaseField.new(name: 'value', type: GraphQL::Types::String, null: true, owner: owner) do
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
index 1124868bdae..773df9b20ee 100644
--- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
@@ -50,17 +50,16 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do
end
before do
- stub_feature_flags(graphql_keyset_pagination_without_next_page_query: false)
allow(GitlabSchema).to receive(:default_max_page_size).and_return(2)
end
- it 'invokes an extra query for the next page check' do
+ it 'invokes no an extra query for the next page check' do
arguments[:first] = 1
subject.nodes
count = ActiveRecord::QueryRecorder.new { subject.has_next_page }.count
- expect(count).to eq(1)
+ expect(count).to eq(0)
end
context 'when the relation is loaded' do
@@ -438,382 +437,4 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do
end
end
end
-
- # duplicated tests, remove with the removal of the graphql_keyset_pagination_without_next_page_query FF
- context 'when the graphql_keyset_pagination_without_next_page_query is on' do
- let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) }
-
- before do
- stub_feature_flags(graphql_keyset_pagination_without_next_page_query: true)
- end
-
- it 'does not invoke an extra query for the next page check' do
- arguments[:first] = 1
-
- subject.nodes
-
- count = ActiveRecord::QueryRecorder.new { subject.has_next_page }.count
- expect(count).to eq(0)
- end
-
- it_behaves_like 'a connection with collection methods'
-
- it_behaves_like 'a redactable connection' do
- let_it_be(:projects) { create_list(:project, 2) }
- let(:unwanted) { projects.second }
- end
-
- describe '#cursor_for' do
- let(:project) { create(:project) }
- let(:cursor) { connection.cursor_for(project) }
-
- it 'returns an encoded ID' do
- expect(decoded_cursor(cursor)).to eq('id' => project.id.to_s)
- end
-
- context 'when an order is specified' do
- let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) }
-
- it 'returns the encoded value of the order' do
- expect(decoded_cursor(cursor)).to include('id' => project.id.to_s)
- end
- end
-
- context 'when multiple orders are specified' do
- let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_updated_at, column_order_created_at, column_order_id])) }
-
- it 'returns the encoded value of the order' do
- expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect))
- end
- end
- end
-
- describe '#sliced_nodes' do
- let(:projects) { create_list(:project, 4) }
-
- context 'when before is passed' do
- let(:arguments) { { before: encoded_cursor(projects[1]) } }
-
- it 'only returns the project before the selected one' do
- expect(subject.sliced_nodes).to contain_exactly(projects.first)
- end
-
- context 'when the sort order is descending' do
- let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) }
-
- it 'returns the correct nodes' do
- expect(subject.sliced_nodes).to contain_exactly(*projects[2..])
- end
- end
- end
-
- context 'when after is passed' do
- let(:arguments) { { after: encoded_cursor(projects[1]) } }
-
- it 'only returns the project before the selected one' do
- expect(subject.sliced_nodes).to contain_exactly(*projects[2..])
- end
-
- context 'when the sort order is descending' do
- let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) }
-
- it 'returns the correct nodes' do
- expect(subject.sliced_nodes).to contain_exactly(projects.first)
- end
- end
- end
-
- context 'when both before and after are passed' do
- let(:arguments) do
- {
- after: encoded_cursor(projects[1]),
- before: encoded_cursor(projects[3])
- }
- end
-
- it 'returns the expected set' do
- expect(subject.sliced_nodes).to contain_exactly(projects[2])
- end
- end
-
- shared_examples 'nodes are in ascending order' do
- context 'when no cursor is passed' do
- let(:arguments) { {} }
-
- it 'returns projects in ascending order' do
- expect(subject.sliced_nodes).to eq(ascending_nodes)
- end
- end
-
- context 'when before cursor value is not NULL' do
- let(:arguments) { { before: encoded_cursor(ascending_nodes[2]) } }
-
- it 'returns all projects before the cursor' do
- expect(subject.sliced_nodes).to eq(ascending_nodes.first(2))
- end
- end
-
- context 'when after cursor value is not NULL' do
- let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } }
-
- it 'returns all projects after the cursor' do
- expect(subject.sliced_nodes).to eq(ascending_nodes.last(3))
- end
- end
-
- context 'when before and after cursor' do
- let(:arguments) { { before: encoded_cursor(ascending_nodes.last), after: encoded_cursor(ascending_nodes.first) } }
-
- it 'returns all projects after the cursor' do
- expect(subject.sliced_nodes).to eq(ascending_nodes[1..3])
- end
- end
- end
-
- shared_examples 'nodes are in descending order' do
- context 'when no cursor is passed' do
- let(:arguments) { {} }
-
- it 'only returns projects in descending order' do
- expect(subject.sliced_nodes).to eq(descending_nodes)
- end
- end
-
- context 'when before cursor value is not NULL' do
- let(:arguments) { { before: encoded_cursor(descending_nodes[2]) } }
-
- it 'returns all projects before the cursor' do
- expect(subject.sliced_nodes).to eq(descending_nodes.first(2))
- end
- end
-
- context 'when after cursor value is not NULL' do
- let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } }
-
- it 'returns all projects after the cursor' do
- expect(subject.sliced_nodes).to eq(descending_nodes.last(3))
- end
- end
-
- context 'when before and after cursor' do
- let(:arguments) { { before: encoded_cursor(descending_nodes.last), after: encoded_cursor(descending_nodes.first) } }
-
- it 'returns all projects after the cursor' do
- expect(subject.sliced_nodes).to eq(descending_nodes[1..3])
- end
- end
- end
-
- context 'when multiple orders with nil values are defined' do
- let_it_be(:project1) { create(:project, last_repository_check_at: 10.days.ago) } # Asc: project5 Desc: project3
- let_it_be(:project2) { create(:project, last_repository_check_at: nil) } # Asc: project1 Desc: project1
- let_it_be(:project3) { create(:project, last_repository_check_at: 5.days.ago) } # Asc: project3 Desc: project5
- let_it_be(:project4) { create(:project, last_repository_check_at: nil) } # Asc: project2 Desc: project2
- let_it_be(:project5) { create(:project, last_repository_check_at: 20.days.ago) } # Asc: project4 Desc: project4
-
- context 'when ascending' do
- let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo, column_order_id]) }
- let_it_be(:nodes) { Project.order(order) }
- let_it_be(:ascending_nodes) { [project5, project1, project3, project2, project4] }
-
- it_behaves_like 'nodes are in ascending order'
-
- context 'when before cursor value is NULL' do
- let(:arguments) { { before: encoded_cursor(project4) } }
-
- it 'returns all projects before the cursor' do
- expect(subject.sliced_nodes).to eq([project5, project1, project3, project2])
- end
- end
-
- context 'when after cursor value is NULL' do
- let(:arguments) { { after: encoded_cursor(project2) } }
-
- it 'returns all projects after the cursor' do
- expect(subject.sliced_nodes).to eq([project4])
- end
- end
- end
-
- context 'when descending' do
- let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo_desc, column_order_id]) }
- let_it_be(:nodes) { Project.order(order) }
- let_it_be(:descending_nodes) { [project3, project1, project5, project2, project4] }
-
- it_behaves_like 'nodes are in descending order'
-
- context 'when before cursor value is NULL' do
- let(:arguments) { { before: encoded_cursor(project4) } }
-
- it 'returns all projects before the cursor' do
- expect(subject.sliced_nodes).to eq([project3, project1, project5, project2])
- end
- end
-
- context 'when after cursor value is NULL' do
- let(:arguments) { { after: encoded_cursor(project2) } }
-
- it 'returns all projects after the cursor' do
- expect(subject.sliced_nodes).to eq([project4])
- end
- end
- end
- end
-
- context 'when ordering by similarity' do
- let_it_be(:project1) { create(:project, name: 'test') }
- let_it_be(:project2) { create(:project, name: 'testing') }
- let_it_be(:project3) { create(:project, name: 'tests') }
- let_it_be(:project4) { create(:project, name: 'testing stuff') }
- let_it_be(:project5) { create(:project, name: 'test') }
-
- let_it_be(:nodes) do
- # Note: sorted_by_similarity_desc scope internally supports the generic keyset order.
- Project.sorted_by_similarity_desc('test', include_in_select: true)
- end
-
- let_it_be(:descending_nodes) { nodes.to_a }
-
- it_behaves_like 'nodes are in descending order'
- end
-
- context 'when an invalid cursor is provided' do
- let(:arguments) { { before: Base64Bp.urlsafe_encode64('invalidcursor', padding: false) } }
-
- it 'raises an error' do
- expect { subject.sliced_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
- end
- end
- end
-
- describe '#nodes' do
- let_it_be(:all_nodes) { create_list(:project, 5) }
-
- let(:paged_nodes) { subject.nodes }
-
- it_behaves_like 'connection with paged nodes' do
- let(:paged_nodes_size) { 3 }
- end
-
- context 'when both are passed' do
- let(:arguments) { { first: 2, last: 2 } }
-
- it 'raises an error' do
- expect { paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
- end
- end
-
- context 'when primary key is not in original order' do
- let(:nodes) { Project.order(last_repository_check_at: :desc) }
-
- it 'is added to end' do
- sliced = subject.sliced_nodes
-
- order_sql = sliced.order_values.last.to_sql
-
- expect(order_sql).to end_with(Project.arel_table[:id].desc.to_sql)
- end
- end
-
- context 'when there is no primary key' do
- before do
- stub_const('NoPrimaryKey', Class.new(ActiveRecord::Base))
- NoPrimaryKey.class_eval do
- self.table_name = 'no_primary_key'
- self.primary_key = nil
- end
- end
-
- let(:nodes) { NoPrimaryKey.all }
-
- it 'raises an error' do
- expect(NoPrimaryKey.primary_key).to be_nil
- expect { subject.sliced_nodes }.to raise_error(ArgumentError, 'Relation must have a primary key')
- end
- end
- end
-
- describe '#has_previous_page and #has_next_page' do
- # using a list of 5 items with a max_page of 3
- let_it_be(:project_list) { create_list(:project, 5) }
- let_it_be(:nodes) { Project.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) }
-
- context 'when default query' do
- let(:arguments) { {} }
-
- it 'has no previous, but a next' do
- expect(subject.has_previous_page).to be_falsey
- expect(subject.has_next_page).to be_truthy
- end
- end
-
- context 'when before is first item' do
- let(:arguments) { { before: encoded_cursor(project_list.first) } }
-
- it 'has no previous, but a next' do
- expect(subject.has_previous_page).to be_falsey
- expect(subject.has_next_page).to be_truthy
- end
- end
-
- describe 'using `before`' do
- context 'when before is the last item' do
- let(:arguments) { { before: encoded_cursor(project_list.last) } }
-
- it 'has no previous, but a next' do
- expect(subject.has_previous_page).to be_falsey
- expect(subject.has_next_page).to be_truthy
- end
- end
-
- context 'when before and last specified' do
- let(:arguments) { { before: encoded_cursor(project_list.last), last: 2 } }
-
- it 'has a previous and a next' do
- expect(subject.has_previous_page).to be_truthy
- expect(subject.has_next_page).to be_truthy
- end
- end
-
- context 'when before and last does request all remaining nodes' do
- let(:arguments) { { before: encoded_cursor(project_list[1]), last: 3 } }
-
- it 'has a previous and a next' do
- expect(subject.has_previous_page).to be_falsey
- expect(subject.has_next_page).to be_truthy
- expect(subject.nodes).to eq [project_list[0]]
- end
- end
- end
-
- describe 'using `after`' do
- context 'when after is the first item' do
- let(:arguments) { { after: encoded_cursor(project_list.first) } }
-
- it 'has a previous, and a next' do
- expect(subject.has_previous_page).to be_truthy
- expect(subject.has_next_page).to be_truthy
- end
- end
-
- context 'when after and first specified' do
- let(:arguments) { { after: encoded_cursor(project_list.first), first: 2 } }
-
- it 'has a previous and a next' do
- expect(subject.has_previous_page).to be_truthy
- expect(subject.has_next_page).to be_truthy
- end
- end
-
- context 'when before and last does request all remaining nodes' do
- let(:arguments) { { after: encoded_cursor(project_list[2]), last: 3 } }
-
- it 'has a previous but no next' do
- expect(subject.has_previous_page).to be_truthy
- expect(subject.has_next_page).to be_falsey
- end
- end
- end
- end
- end
end
diff --git a/spec/lib/gitlab/http_connection_adapter_spec.rb b/spec/lib/gitlab/http_connection_adapter_spec.rb
index a241a4b6490..5e2c6be8993 100644
--- a/spec/lib/gitlab/http_connection_adapter_spec.rb
+++ b/spec/lib/gitlab/http_connection_adapter_spec.rb
@@ -124,5 +124,16 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
expect(connection.port).to eq(443)
end
end
+
+ context 'when URL scheme is not HTTP/HTTPS' do
+ let(:uri) { URI('ssh://example.org') }
+
+ it 'raises error' do
+ expect { subject }.to raise_error(
+ Gitlab::HTTP::BlockedUrlError,
+ "URL 'ssh://example.org' is blocked: Only allowed schemes are http, https"
+ )
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/i18n_spec.rb b/spec/lib/gitlab/i18n_spec.rb
index aae4a13bd73..b752d89bf0d 100644
--- a/spec/lib/gitlab/i18n_spec.rb
+++ b/spec/lib/gitlab/i18n_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::I18n do
describe '.selectable_locales' do
include StubLanguagesTranslationPercentage
- it 'does not return languages with low translation levels' do
+ it 'does not return languages with default translation levels 60%' do
stub_languages_translation_percentage(pt_BR: 0, en: 100, es: 65)
expect(described_class.selectable_locales).to eq({
@@ -16,6 +16,12 @@ RSpec.describe Gitlab::I18n do
'es' => 'Spanish - español'
})
end
+
+ it 'does not return languages with less than 100% translation levels' do
+ stub_languages_translation_percentage(pt_BR: 0, en: 100, es: 65)
+
+ expect(described_class.selectable_locales(100)).to eq({ 'en' => 'English' })
+ end
end
describe '.locale=' do
diff --git a/spec/lib/gitlab/import/merge_request_helpers_spec.rb b/spec/lib/gitlab/import/merge_request_helpers_spec.rb
index f858ab934bb..9626b7b893f 100644
--- a/spec/lib/gitlab/import/merge_request_helpers_spec.rb
+++ b/spec/lib/gitlab/import/merge_request_helpers_spec.rb
@@ -37,11 +37,8 @@ RSpec.describe Gitlab::Import::MergeRequestHelpers, type: :helper do
attributes.merge(iid: iid, source_branch: iid.to_s))
end
- # does ensure that we only load object twice
- # 1. by #insert_and_return_id
- # 2. by project.merge_requests.find
- expect_any_instance_of(MergeRequest).to receive(:attributes)
- .twice.times.and_call_original
+ # ensures that we only load object once by project.merge_requests.find
+ expect(MergeRequest).to receive(:allocate).once.and_call_original
expect(subject.first).not_to be_nil
expect(subject.second).to eq(false)
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index e9dde1c6180..b34399d20f1 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -157,6 +157,7 @@ merge_requests:
- author
- assignee
- reviewers
+- reviewed_by_users
- updated_by
- milestone
- iteration
@@ -408,6 +409,20 @@ project:
- boards
- last_event
- integrations
+- push_hooks_integrations
+- tag_push_hooks_integrations
+- issue_hooks_integrations
+- confidential_issue_hooks_integrations
+- merge_request_hooks_integrations
+- note_hooks_integrations
+- confidential_note_hooks_integrations
+- job_hooks_integrations
+- archive_trace_hooks_integrations
+- pipeline_hooks_integrations
+- wiki_page_hooks_integrations
+- deployment_hooks_integrations
+- alert_hooks_integrations
+- vulnerability_hooks_integrations
- campfire_integration
- confluence_integration
- datadog_integration
@@ -423,7 +438,6 @@ project:
- packagist_integration
- pivotaltracker_integration
- prometheus_integration
-- flowdock_integration
- assembla_integration
- asana_integration
- slack_integration
@@ -649,6 +663,7 @@ project:
- project_callouts
- pipeline_metadata
- disable_download_button
+- dependency_list_exports
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb
index dbd6cb243f6..a5b03974bc0 100644
--- a/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeRestorer do
let(:group) { create(:group) }
let(:shared) { Gitlab::ImportExport::Shared.new(group) }
let(:group_tree_restorer) { described_class.new(user: importer_user, shared: shared, group: group, group_hash: nil) }
- let(:group_json) { Gitlab::Json.parse(IO.read(File.join(shared.export_path, 'group.json'))) }
+ let(:group_json) { Gitlab::Json.parse(File.read(File.join(shared.export_path, 'group.json'))) }
shared_examples 'excluded attributes' do
excluded_attributes = %w[
diff --git a/spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb b/spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb
index 31d647f883a..f5a4fc79c90 100644
--- a/spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb
@@ -154,6 +154,6 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeSaver do
end
def group_json(filename)
- ::JSON.parse(IO.read(filename))
+ ::JSON.parse(File.read(filename))
end
end
diff --git a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
index 1444897e136..aa30e24296e 100644
--- a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do
+RSpec.describe Gitlab::ImportExport::Group::TreeRestorer, feature: :subgroups do
include ImportExport::CommonUtil
shared_examples 'group restoration' do
@@ -112,7 +112,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do
let(:shared) { Gitlab::ImportExport::Shared.new(group) }
let(:group_tree_restorer) { described_class.new(user: importer_user, shared: shared, group: group) }
let(:exported_file) { File.join(shared.export_path, 'tree/groups/4352.json') }
- let(:group_json) { Gitlab::Json.parse(IO.read(exported_file)) }
+ let(:group_json) { Gitlab::Json.parse(File.read(exported_file)) }
shared_examples 'excluded attributes' do
excluded_attributes = %w[
diff --git a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
index b1f5574fba1..d8a4230e5da 100644
--- a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
+++ b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe 'Test coverage of the Project Import' do
end
def relations_from_json(json_file)
- json = Gitlab::Json.parse(IO.read(json_file))
+ json = Gitlab::Json.parse(File.read(json_file))
[].tap { |res| gather_relations({ project: json }, res, []) }
.map { |relation_names| relation_names.join('.') }
diff --git a/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb b/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
index ed4368ba802..e8ecd98b1e1 100644
--- a/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
@@ -96,6 +96,6 @@ RSpec.describe Gitlab::ImportExport::Json::LegacyWriter do
def subject_json
subject.close
- ::JSON.parse(IO.read(subject.path))
+ ::JSON.parse(File.read(subject.path))
end
end
diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
index aa456736f78..5b6f50025ff 100644
--- a/spec/lib/gitlab/import_export/lfs_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do
let(:lfs_json_file) { File.join(shared.export_path, Gitlab::ImportExport.lfs_objects_filename) }
def lfs_json
- Gitlab::Json.parse(IO.read(lfs_json_file))
+ Gitlab::Json.parse(File.read(lfs_json_file))
end
before do
diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb
index 34591122a97..4f01f470ce7 100644
--- a/spec/lib/gitlab/import_export/model_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb
@@ -23,8 +23,8 @@ RSpec.describe 'Import/Export model configuration' do
# List of current models between models, in the format of
# {model: [model_2, model3], ...}
def setup_models
- model_names.each_with_object({}) do |model_name, hash|
- hash[model_name] = associations_for(relation_class_for_name(model_name)) - ['project']
+ model_names.index_with do |model_name|
+ associations_for(relation_class_for_name(model_name)) - ['project']
end
end
diff --git a/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb b/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb
index a781139acab..d70e89c6856 100644
--- a/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb
+++ b/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb
@@ -8,17 +8,17 @@ RSpec.describe Gitlab::ImportExport::Project::ExportedRelationsMerger do
let(:shared) { Gitlab::ImportExport::Shared.new(export_job.project) }
before do
- create(:project_relation_export_upload,
+ create(:relation_export_upload,
relation_export: create(:project_relation_export, relation: 'project', project_export_job: export_job),
export_file: fixture_file_upload("spec/fixtures/gitlab/import_export/project.tar.gz")
)
- create(:project_relation_export_upload,
+ create(:relation_export_upload,
relation_export: create(:project_relation_export, relation: 'labels', project_export_job: export_job),
export_file: fixture_file_upload("spec/fixtures/gitlab/import_export/labels.tar.gz")
)
- create(:project_relation_export_upload,
+ create(:relation_export_upload,
relation_export: create(:project_relation_export, relation: 'uploads', project_export_job: export_job),
export_file: fixture_file_upload("spec/fixtures/gitlab/import_export/uploads.tar.gz")
)
diff --git a/spec/lib/gitlab/import_export/project/relation_saver_spec.rb b/spec/lib/gitlab/import_export/project/relation_saver_spec.rb
index 0467b63e918..5032dd864bb 100644
--- a/spec/lib/gitlab/import_export/project/relation_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/relation_saver_spec.rb
@@ -111,7 +111,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationSaver do
end
def read_json(path)
- Gitlab::Json.parse(IO.read(path))
+ Gitlab::Json.parse(File.read(path))
end
def read_ndjson(path)
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index b753746cd8c..2699dc10b18 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -768,7 +768,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
it 'overrides project feature access levels' do
access_level_keys = ProjectFeature.available_features.map { |feature| ProjectFeature.access_level_attribute(feature) }
- disabled_access_levels = access_level_keys.to_h { |item| [item, 'disabled'] }
+ disabled_access_levels = access_level_keys.index_with { 'disabled' }
project.create_import_data(data: { override_params: disabled_access_levels })
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index 727ca4f630b..3da7af7509e 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
end
- it 'restores the repo successfully' do
+ it 'restores the repo successfully', :aggregate_failures do
expect(project.repository.exists?).to be false
expect(subject.restore).to be_truthy
@@ -42,7 +42,7 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
end
context 'when the repository already exists' do
- it 'deletes the existing repository before importing' do
+ it 'deletes the existing repository before importing', :aggregate_failures do
allow(project.repository).to receive(:exists?).and_return(true)
allow(project.repository).to receive(:disk_path).and_return('repository_path')
@@ -69,7 +69,7 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
end
- it 'restores the wiki repo successfully' do
+ it 'restores the wiki repo successfully', :aggregate_failures do
expect(project.wiki_repository_exists?).to be false
subject.restore
@@ -83,10 +83,21 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(exportable: project_without_wiki, shared: shared) }
- it 'does not creates an empty wiki' do
+ it 'does not creates an empty wiki', :aggregate_failures do
expect(subject.restore).to be true
expect(project.wiki_repository_exists?).to be false
end
end
+
+ context 'when wiki already exists' do
+ subject do
+ described_class.new(path_to_bundle: bundle_path, shared: shared, importable: ProjectWiki.new(project_with_repo))
+ end
+
+ it 'does not cause an error when restoring', :aggregate_failures do
+ expect(subject.restore).to be true
+ expect(shared.errors).to be_empty
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb
index f5eed81f73c..a34e68ecd19 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_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index 41ffcece221..393e0a9be10 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -11,7 +11,6 @@ RSpec.describe Gitlab::ImportSources do
'Bitbucket Cloud' => 'bitbucket',
'Bitbucket Server' => 'bitbucket_server',
'GitLab.com' => 'gitlab',
- 'Google Code' => 'google_code',
'FogBugz' => 'fogbugz',
'Repository by URL' => 'git',
'GitLab export' => 'gitlab_project',
@@ -32,7 +31,6 @@ RSpec.describe Gitlab::ImportSources do
bitbucket
bitbucket_server
gitlab
- google_code
fogbugz
git
gitlab_project
@@ -69,7 +67,6 @@ RSpec.describe Gitlab::ImportSources do
'bitbucket' => Gitlab::BitbucketImport::Importer,
'bitbucket_server' => Gitlab::BitbucketServerImport::Importer,
'gitlab' => Gitlab::GitlabImport::Importer,
- 'google_code' => nil,
'fogbugz' => Gitlab::FogbugzImport::Importer,
'git' => nil,
'gitlab_project' => Gitlab::ImportExport::Importer,
@@ -91,7 +88,6 @@ RSpec.describe Gitlab::ImportSources do
'bitbucket' => 'Bitbucket Cloud',
'bitbucket_server' => 'Bitbucket Server',
'gitlab' => 'GitLab.com',
- 'google_code' => 'Google Code',
'fogbugz' => 'FogBugz',
'git' => 'Repository by URL',
'gitlab_project' => 'GitLab export',
diff --git a/spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb b/spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb
index c5288b9afbc..e2c67c68eb7 100644
--- a/spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb
+++ b/spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb
@@ -10,8 +10,8 @@ RSpec.describe Gitlab::IncidentManagement::PagerDuty::IncidentIssueDescription d
[{ 'summary' => 'Laura Haley', 'url' => 'https://webdemo.pagerduty.com/users/P553OPV' }]
end
- let(:impacted_services) do
- [{ 'summary' => 'Production XDB Cluster', 'url' => 'https://webdemo.pagerduty.com/services/PN49J75' }]
+ let(:impacted_service) do
+ { 'summary' => 'Production XDB Cluster', 'url' => 'https://webdemo.pagerduty.com/services/PN49J75' }
end
let(:incident_payload) do
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::IncidentManagement::PagerDuty::IncidentIssueDescription d
'urgency' => 'high',
'incident_key' => 'SOME-KEY',
'assignees' => assignees,
- 'impacted_services' => impacted_services
+ 'impacted_service' => impacted_service
}
end
@@ -40,7 +40,7 @@ RSpec.describe Gitlab::IncidentManagement::PagerDuty::IncidentIssueDescription d
**Incident key:** SOME-KEY#{markdown_line_break}
**Created at:** 26 September 2017, 3:14PM (UTC)#{markdown_line_break}
**Assignees:** [Laura Haley](https://webdemo.pagerduty.com/users/P553OPV)#{markdown_line_break}
- **Impacted services:** [Production XDB Cluster](https://webdemo.pagerduty.com/services/PN49J75)
+ **Impacted service:** [Production XDB Cluster](https://webdemo.pagerduty.com/services/PN49J75)
MARKDOWN
)
end
@@ -78,18 +78,15 @@ RSpec.describe Gitlab::IncidentManagement::PagerDuty::IncidentIssueDescription d
end
end
- context 'when there are several impacted services' do
- let(:impacted_services) do
- [
- { 'summary' => 'XDB Cluster', 'url' => 'https://xdb.pagerduty.com' },
- { 'summary' => 'BRB Cluster', 'url' => 'https://brb.pagerduty.com' }
- ]
+ context 'when there is an impacted service' do
+ let(:impacted_service) do
+ { 'summary' => 'XDB Cluster', 'url' => 'https://xdb.pagerduty.com' }
end
- it 'impacted services is a list of links' do
+ it 'impacted service is a single link' do
expect(to_s).to include(
<<~MARKDOWN.chomp
- **Impacted services:** [XDB Cluster](https://xdb.pagerduty.com), [BRB Cluster](https://brb.pagerduty.com)
+ **Impacted service:** [XDB Cluster](https://xdb.pagerduty.com)
MARKDOWN
)
end
diff --git a/spec/lib/gitlab/instrumentation/redis_base_spec.rb b/spec/lib/gitlab/instrumentation/redis_base_spec.rb
index f9dd0c94c97..656e6ffba05 100644
--- a/spec/lib/gitlab/instrumentation/redis_base_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_base_spec.rb
@@ -1,8 +1,10 @@
# frozen_string_literal: true
require 'spec_helper'
+require 'rspec-parameterized'
RSpec.describe Gitlab::Instrumentation::RedisBase, :request_store do
+ using RSpec::Parameterized::TableSyntax
let(:instrumentation_class_a) do
stub_const('InstanceA', Class.new(described_class))
end
@@ -88,6 +90,44 @@ RSpec.describe Gitlab::Instrumentation::RedisBase, :request_store do
end
end
+ describe '.increment_cross_slot_request_count' do
+ context 'storage key overlapping' do
+ it 'keys do not overlap across storages' do
+ 3.times { instrumentation_class_a.increment_cross_slot_request_count }
+ 2.times { instrumentation_class_b.increment_cross_slot_request_count }
+
+ expect(instrumentation_class_a.get_cross_slot_request_count).to eq(3)
+ expect(instrumentation_class_b.get_cross_slot_request_count).to eq(2)
+ end
+
+ it 'increments by the given amount' do
+ instrumentation_class_a.increment_cross_slot_request_count(2)
+ instrumentation_class_a.increment_cross_slot_request_count(3)
+
+ expect(instrumentation_class_a.get_cross_slot_request_count).to eq(5)
+ end
+ end
+ end
+
+ describe '.increment_allowed_cross_slot_request_count' do
+ context 'storage key overlapping' do
+ it 'keys do not overlap across storages' do
+ 3.times { instrumentation_class_a.increment_allowed_cross_slot_request_count }
+ 2.times { instrumentation_class_b.increment_allowed_cross_slot_request_count }
+
+ expect(instrumentation_class_a.get_allowed_cross_slot_request_count).to eq(3)
+ expect(instrumentation_class_b.get_allowed_cross_slot_request_count).to eq(2)
+ end
+
+ it 'increments by the given amount' do
+ instrumentation_class_a.increment_allowed_cross_slot_request_count(2)
+ instrumentation_class_a.increment_allowed_cross_slot_request_count(3)
+
+ expect(instrumentation_class_a.get_allowed_cross_slot_request_count).to eq(5)
+ end
+ end
+ end
+
describe '.increment_read_bytes' do
context 'storage key overlapping' do
it 'keys do not overlap across storages' do
@@ -130,4 +170,44 @@ RSpec.describe Gitlab::Instrumentation::RedisBase, :request_store do
end
end
end
+
+ describe '.redis_cluster_validate!' do
+ let(:args) { [[:mget, 'foo', 'bar']] }
+
+ before do
+ instrumentation_class_a.enable_redis_cluster_validation
+ end
+
+ context 'Rails environments' do
+ where(:env, :allowed, :should_raise) do
+ 'production' | false | false
+ 'production' | true | false
+ 'staging' | false | false
+ 'staging' | true | false
+ 'development' | true | false
+ 'development' | false | true
+ 'test' | true | false
+ 'test' | false | true
+ end
+
+ with_them do
+ it do
+ stub_rails_env(env)
+
+ validation = -> { instrumentation_class_a.redis_cluster_validate!(args) }
+ under_test = if allowed
+ -> { Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands(&validation) }
+ else
+ validation
+ end
+
+ if should_raise
+ expect(&under_test).to raise_error(::Gitlab::Instrumentation::RedisClusterValidator::CrossSlotError)
+ else
+ expect(&under_test).not_to raise_error
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
index 58c75bff9dd..892b8e69124 100644
--- a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
@@ -7,107 +7,102 @@ require 'rspec-parameterized'
RSpec.describe Gitlab::Instrumentation::RedisClusterValidator do
include RailsHelpers
- describe '.validate!' do
+ describe '.validate' do
using RSpec::Parameterized::TableSyntax
- context 'Rails environments' do
- where(:env, :should_raise) do
- 'production' | false
- 'staging' | false
- 'development' | true
- 'test' | true
- end
-
- with_them do
- it do
- stub_rails_env(env)
-
- args = [[:mget, 'foo', 'bar']]
-
- if should_raise
- expect { described_class.validate!(args) }
- .to raise_error(described_class::CrossSlotError)
- else
- expect { described_class.validate!(args) }.not_to raise_error
- end
- end
- end
- end
-
- where(:command, :arguments, :should_raise) do
- :rename | %w(foo bar) | true
- :RENAME | %w(foo bar) | true
- 'rename' | %w(foo bar) | true
- 'RENAME' | %w(foo bar) | true
- :rename | %w(iaa ahy) | false # 'iaa' and 'ahy' hash to the same slot
- :rename | %w({foo}:1 {foo}:2) | false
- :rename | %w(foo foo bar) | false # This is not a valid command but should not raise here
- :mget | %w(foo bar) | true
- :mget | %w(foo foo bar) | true
- :mget | %w(foo foo) | false
- :blpop | %w(foo bar 1) | true
- :blpop | %w(foo foo 1) | false
- :mset | %w(foo a bar a) | true
- :mset | %w(foo a foo a) | false
- :del | %w(foo bar) | true
- :del | [%w(foo bar)] | true # Arguments can be a nested array
- :del | %w(foo foo) | false
- :hset | %w(foo bar) | false # Not a multi-key command
- :mget | [] | false # This is invalid, but not because it's a cross-slot command
+ 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
+ :mget | [] | 0 | true # This is invalid, but not because it's a cross-slot command
end
with_them do
it do
args = [[command] + arguments]
-
- if should_raise
- expect { described_class.validate!(args) }
- .to raise_error(described_class::CrossSlotError)
+ if is_valid.nil?
+ expect(described_class.validate(args)).to eq(nil)
else
- expect { described_class.validate!(args) }.not_to raise_error
+ expect(described_class.validate(args)[:valid]).to eq(is_valid)
+ expect(described_class.validate(args)[:allowed]).to eq(false)
+ expect(described_class.validate(args)[:command_name]).to eq(command.to_s.upcase)
+ expect(described_class.validate(args)[:key_count]).to eq(keys)
end
end
end
- where(:arguments, :should_raise) do
- [[:get, "foo"], [:get, "bar"]] | true
- [[:get, "foo"], [:mget, "foo", "bar"]] | true # mix of single-key and multi-key cmds
- [[:get, "{foo}:name"], [:get, "{foo}:profile"]] | false
- [[:del, "foo"], [:del, "bar"]] | true
- [] | false # pipeline or transaction opened and closed without ops
+ where(:arguments, :should_raise, :output) do
+ [
+ [
+ [[:get, "foo"], [:get, "bar"]],
+ true,
+ { valid: false, key_count: 2, command_name: 'PIPELINE/MULTI', allowed: false }
+ ],
+ [
+ [[:get, "foo"], [:mget, "foo", "bar"]],
+ true,
+ { valid: false, key_count: 3, command_name: 'PIPELINE/MULTI', allowed: false }
+ ],
+ [
+ [[:get, "{foo}:name"], [:get, "{foo}:profile"]],
+ false,
+ { valid: true, key_count: 2, command_name: 'PIPELINE/MULTI', allowed: false }
+ ],
+ [
+ [[:del, "foo"], [:del, "bar"]],
+ true,
+ { valid: false, key_count: 2, command_name: 'PIPELINE/MULTI', allowed: false }
+ ],
+ [
+ [],
+ false,
+ nil # pipeline or transaction opened and closed without ops
+ ]
+ ]
end
with_them do
it do
- if should_raise
- expect { described_class.validate!(arguments) }
- .to raise_error(described_class::CrossSlotError)
- else
- expect { described_class.validate!(arguments) }.not_to raise_error
- end
+ expect(described_class.validate(arguments)).to eq(output)
end
end
end
describe '.allow_cross_slot_commands' do
- it 'does not raise for invalid arguments' do
- expect do
+ it 'skips validation for allowed commands' do
+ expect(
described_class.allow_cross_slot_commands do
- described_class.validate!([[:mget, 'foo', 'bar']])
+ described_class.validate([[:mget, 'foo', 'bar']])
end
- end.not_to raise_error
+ ).to eq({ valid: true, key_count: 2, command_name: 'MGET', allowed: true })
end
it 'allows nested invocation' do
- expect do
+ expect(
described_class.allow_cross_slot_commands do
described_class.allow_cross_slot_commands do
- described_class.validate!([[:mget, 'foo', 'bar']])
+ described_class.validate([[:mget, 'foo', 'bar']])
end
- described_class.validate!([[:mget, 'foo', 'bar']])
+ described_class.validate([[:mget, 'foo', 'bar']])
end
- end.not_to raise_error
+ ).to eq({ valid: true, key_count: 2, command_name: 'MGET', allowed: true })
end
end
end
diff --git a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
index 02c5dfb7521..187a6ff1739 100644
--- a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
@@ -2,6 +2,7 @@
require 'spec_helper'
require 'rspec-parameterized'
+require 'support/helpers/rails_helpers'
RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_shared_state, :request_store do
using RSpec::Parameterized::TableSyntax
@@ -74,6 +75,47 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
end
end.to raise_exception(Redis::CommandError)
end
+
+ context 'in production environment' do
+ before do
+ stub_rails_env('production') # to avoid raising CrossSlotError
+ end
+
+ it 'counts disallowed cross-slot requests' do
+ expect(instrumentation_class).to receive(:increment_cross_slot_request_count).and_call_original
+ expect(instrumentation_class).not_to receive(:increment_allowed_cross_slot_request_count).and_call_original
+
+ Gitlab::Redis::SharedState.with { |redis| redis.call(:mget, 'foo', 'bar') }
+ end
+
+ it 'does not count allowed cross-slot requests' do
+ expect(instrumentation_class).not_to receive(:increment_cross_slot_request_count).and_call_original
+ expect(instrumentation_class).to receive(:increment_allowed_cross_slot_request_count).and_call_original
+
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ Gitlab::Redis::SharedState.with { |redis| redis.call(:mget, 'foo', 'bar') }
+ end
+ end
+
+ it 'skips count for non-cross-slot requests' do
+ expect(instrumentation_class).not_to receive(:increment_cross_slot_request_count).and_call_original
+ expect(instrumentation_class).not_to receive(:increment_allowed_cross_slot_request_count).and_call_original
+
+ Gitlab::Redis::SharedState.with { |redis| redis.call(:mget, '{foo}bar', '{foo}baz') }
+ end
+ end
+
+ context 'without active RequestStore' do
+ before do
+ ::RequestStore.end!
+ end
+
+ it 'still runs cross-slot validation' do
+ expect do
+ Gitlab::Redis::SharedState.with { |redis| redis.mget('foo', 'bar') }
+ end.to raise_error(instance_of(Gitlab::Instrumentation::RedisClusterValidator::CrossSlotError))
+ end
+ end
end
describe 'latency' do
@@ -103,7 +145,7 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
Gitlab::Redis::SharedState.with do |redis|
redis.pipelined do |pipeline|
- pipeline.call(:get, '{foobar}:buz')
+ pipeline.call(:get, '{foobar}buz')
pipeline.call(:get, '{foobar}baz')
end
end
@@ -123,37 +165,36 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
end
describe 'commands not in the apdex' do
- where(:command) do
- [
- [%w[brpop foobar 0.01]],
- [%w[blpop foobar 0.01]],
- [%w[brpoplpush foobar bazqux 0.01]],
- [%w[bzpopmin foobar 0.01]],
- [%w[bzpopmax foobar 0.01]],
- [%w[xread block 1 streams mystream 0-0]],
- [%w[xreadgroup group mygroup myconsumer block 1 streams foobar 0-0]]
- ]
+ where(:setup, :command) do
+ [['rpush', 'foobar', 1]] | ['brpop', 'foobar', 0]
+ [['rpush', 'foobar', 1]] | ['blpop', 'foobar', 0]
+ [['rpush', '{abc}foobar', 1]] | ['brpoplpush', '{abc}foobar', '{abc}bazqux', 0]
+ [['rpush', '{abc}foobar', 1]] | ['brpoplpush', '{abc}foobar', '{abc}bazqux', 0]
+ [['zadd', 'foobar', 1, 'a']] | ['bzpopmin', 'foobar', 0]
+ [['zadd', 'foobar', 1, 'a']] | ['bzpopmax', 'foobar', 0]
+ [['xadd', 'mystream', 1, 'myfield', 'mydata']] | ['xread', 'block', 1, 'streams', 'mystream', '0-0']
+ [['xadd', 'foobar', 1, 'myfield', 'mydata'], ['xgroup', 'create', 'foobar', 'mygroup', 0]] | ['xreadgroup', 'group', 'mygroup', 'myconsumer', 'block', 1, 'streams', 'foobar', '0-0']
end
with_them do
it 'skips requests we do not want in the apdex' do
+ Gitlab::Redis::SharedState.with { |redis| setup.each { |cmd| redis.call(*cmd) } }
+
expect(instrumentation_class).not_to receive(:instance_observe_duration)
- begin
- Gitlab::Redis::SharedState.with { |redis| redis.call(*command) }
- rescue Gitlab::Instrumentation::RedisClusterValidator::CrossSlotError, ::Redis::CommandError
- end
+ Gitlab::Redis::SharedState.with { |redis| redis.call(*command) }
end
end
context 'with pipelined commands' do
- it 'skips requests that have blocking commands', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/373026' do
+ it 'skips requests that have blocking commands' do
expect(instrumentation_class).not_to receive(:instance_observe_duration)
Gitlab::Redis::SharedState.with do |redis|
redis.pipelined do |pipeline|
- pipeline.call(:get, 'foo')
- pipeline.call(:brpop, 'foobar', '0.01')
+ pipeline.call(:get, '{foobar}buz')
+ pipeline.call(:rpush, '{foobar}baz', 1)
+ pipeline.call(:brpop, '{foobar}baz', 0)
end
end
end
diff --git a/spec/lib/gitlab/instrumentation/redis_spec.rb b/spec/lib/gitlab/instrumentation/redis_spec.rb
index c01d06c97b0..3e02eadba4b 100644
--- a/spec/lib/gitlab/instrumentation/redis_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
+require 'support/helpers/rails_helpers'
RSpec.describe Gitlab::Instrumentation::Redis do
def stub_storages(method, value)
@@ -22,6 +23,8 @@ RSpec.describe Gitlab::Instrumentation::Redis do
end
it_behaves_like 'aggregation of redis storage data', :get_request_count
+ it_behaves_like 'aggregation of redis storage data', :get_cross_slot_request_count
+ it_behaves_like 'aggregation of redis storage data', :get_allowed_cross_slot_request_count
it_behaves_like 'aggregation of redis storage data', :query_time
it_behaves_like 'aggregation of redis storage data', :read_bytes
it_behaves_like 'aggregation of redis storage data', :write_bytes
@@ -35,20 +38,28 @@ RSpec.describe Gitlab::Instrumentation::Redis do
Gitlab::Redis::Cache.with { |redis| redis.info }
RequestStore.clear!
- Gitlab::Redis::Cache.with { |redis| redis.set('cache-test', 321) }
+ stub_rails_env('staging') # to avoid raising CrossSlotError
+ Gitlab::Redis::Cache.with { |redis| redis.mset('cache-test', 321, 'cache-test-2', 321) }
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ Gitlab::Redis::Cache.with { |redis| redis.mget('cache-test', 'cache-test-2') }
+ end
Gitlab::Redis::SharedState.with { |redis| redis.set('shared-state-test', 123) }
end
it 'returns payload filtering out zeroed values' do
expected_payload = {
# Aggregated results
- redis_calls: 2,
+ redis_calls: 3,
+ redis_cross_slot_calls: 1,
+ redis_allowed_cross_slot_calls: 1,
redis_duration_s: be >= 0,
redis_read_bytes: be >= 0,
redis_write_bytes: be >= 0,
# Cache results
- redis_cache_calls: 1,
+ redis_cache_calls: 2,
+ redis_cache_cross_slot_calls: 1,
+ redis_cache_allowed_cross_slot_calls: 1,
redis_cache_duration_s: be >= 0,
redis_cache_read_bytes: be >= 0,
redis_cache_write_bytes: be >= 0,
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index d5ff39767c4..7d78d25f18e 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -2,6 +2,7 @@
require 'spec_helper'
require 'rspec-parameterized'
+require 'support/helpers/rails_helpers'
RSpec.describe Gitlab::InstrumentationHelper do
using RSpec::Parameterized::TableSyntax
@@ -38,25 +39,33 @@ RSpec.describe Gitlab::InstrumentationHelper do
context 'when Redis calls are made' do
it 'adds Redis data and omits Gitaly data' do
- Gitlab::Redis::Cache.with { |redis| redis.set('test-cache', 123) }
+ stub_rails_env('staging') # to avoid raising CrossSlotError
+ Gitlab::Redis::Cache.with { |redis| redis.mset('test-cache', 123, 'test-cache2', 123) }
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ Gitlab::Redis::Cache.with { |redis| redis.mget('cache-test', 'cache-test-2') }
+ end
Gitlab::Redis::Queues.with { |redis| redis.set('test-queues', 321) }
subject
# Aggregated payload
- expect(payload[:redis_calls]).to eq(2)
+ expect(payload[:redis_calls]).to eq(3)
+ expect(payload[:redis_cross_slot_calls]).to eq(1)
+ expect(payload[:redis_allowed_cross_slot_calls]).to eq(1)
expect(payload[:redis_duration_s]).to be >= 0
expect(payload[:redis_read_bytes]).to be >= 0
expect(payload[:redis_write_bytes]).to be >= 0
- # Shared state payload
+ # Queue payload
expect(payload[:redis_queues_calls]).to eq(1)
expect(payload[:redis_queues_duration_s]).to be >= 0
expect(payload[:redis_queues_read_bytes]).to be >= 0
expect(payload[:redis_queues_write_bytes]).to be >= 0
# Cache payload
- expect(payload[:redis_cache_calls]).to eq(1)
+ expect(payload[:redis_cache_calls]).to eq(2)
+ expect(payload[:redis_cache_cross_slot_calls]).to eq(1)
+ expect(payload[:redis_cache_allowed_cross_slot_calls]).to eq(1)
expect(payload[:redis_cache_duration_s]).to be >= 0
expect(payload[:redis_cache_read_bytes]).to be >= 0
expect(payload[:redis_cache_write_bytes]).to be >= 0
@@ -67,6 +76,26 @@ RSpec.describe Gitlab::InstrumentationHelper do
end
end
+ context 'when LDAP requests are made' do
+ let(:provider) { 'ldapmain' }
+ let(:adapter) { Gitlab::Auth::Ldap::Adapter.new(provider) }
+ let(:conn) { instance_double(Net::LDAP::Connection, search: search) }
+ let(:search) { double(:search, result_code: 200) } # rubocop: disable RSpec/VerifiedDoubles
+
+ it 'adds LDAP data' do
+ allow_next_instance_of(Net::LDAP) do |net_ldap|
+ allow(net_ldap).to receive(:use_connection).and_yield(conn)
+ end
+
+ adapter.users('uid', 'foo')
+ subject
+
+ # Query count should be 2, as it will call `open` then `search`
+ expect(payload[:net_ldap_count]).to eq(2)
+ expect(payload[:net_ldap_duration_s]).to be >= 0
+ end
+ end
+
context 'when the request matched a Rack::Attack safelist' do
it 'logs the safelist name' do
Gitlab::Instrumentation::Throttle.safelist = 'foobar'
@@ -122,7 +151,7 @@ RSpec.describe Gitlab::InstrumentationHelper do
include MemoryInstrumentationHelper
before do
- skip_memory_instrumentation!
+ verify_memory_instrumentation_available!
end
it 'logs memory usage metrics' do
diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
index 8abd041fd4e..2ead188dc93 100644
--- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
@@ -132,6 +132,14 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do
it_behaves_like 'local address'
end
+ context 'when a non HTTP/HTTPS URL is provided' do
+ let(:api_url) { 'ssh://192.168.1.2' }
+
+ it 'raises an error' do
+ expect { client }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError)
+ end
+ end
+
it 'falls back to default options, but allows overriding' do
client = described_class.new(api_url)
defaults = Gitlab::Kubernetes::KubeClient::DEFAULT_KUBECLIENT_OPTIONS
@@ -149,7 +157,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do
it_behaves_like 'a Kubeclient'
it 'has the core API endpoint' do
- expect(subject.api_endpoint.to_s).to match(%r{\/api\Z})
+ expect(subject.api_endpoint.to_s).to match(%r{/api\Z})
end
it 'has the api_version' do
@@ -163,7 +171,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do
it_behaves_like 'a Kubeclient'
it 'has the RBAC API group endpoint' do
- expect(subject.api_endpoint.to_s).to match(%r{\/apis\/rbac.authorization.k8s.io\Z})
+ expect(subject.api_endpoint.to_s).to match(%r{/apis/rbac.authorization.k8s.io\Z})
end
it 'has the api_version' do
@@ -177,7 +185,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do
it_behaves_like 'a Kubeclient'
it 'has the Istio API group endpoint' do
- expect(subject.api_endpoint.to_s).to match(%r{\/apis\/networking.istio.io\Z})
+ expect(subject.api_endpoint.to_s).to match(%r{/apis/networking.istio.io\Z})
end
it 'has the api_version' do
@@ -191,7 +199,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do
it_behaves_like 'a Kubeclient'
it 'has the extensions API group endpoint' do
- expect(subject.api_endpoint.to_s).to match(%r{\/apis\/serving.knative.dev\Z})
+ expect(subject.api_endpoint.to_s).to match(%r{/apis/serving.knative.dev\Z})
end
it 'has the api_version' do
@@ -205,7 +213,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do
it_behaves_like 'a Kubeclient'
it 'has the networking API group endpoint' do
- expect(subject.api_endpoint.to_s).to match(%r{\/apis\/networking.k8s.io\Z})
+ expect(subject.api_endpoint.to_s).to match(%r{/apis/networking.k8s.io\Z})
end
it 'has the api_version' do
@@ -219,7 +227,7 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do
it_behaves_like 'a Kubeclient'
it 'has the metrics API group endpoint' do
- expect(subject.api_endpoint.to_s).to match(%r{\/apis\/metrics.k8s.io\Z})
+ expect(subject.api_endpoint.to_s).to match(%r{/apis/metrics.k8s.io\Z})
end
it 'has the api_version' do
diff --git a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
index 57f2b1cfd96..81d423598f2 100644
--- a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
+++ b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
@@ -2,6 +2,8 @@
require 'spec_helper'
RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do
+ let_it_be(:project) { create(:project) }
+
let(:klass) do
Class.new(ActiveRecord::Base) do
self.table_name = 'issues'
@@ -17,7 +19,12 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do
end
let(:cache_version) { Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 }
- let(:thing) { klass.create!(title: markdown, title_html: html, cached_markdown_version: cache_version) }
+ let(:thing) do
+ klass.create!(
+ project_id: project.id, namespace_id: project.project_namespace_id,
+ title: markdown, title_html: html, cached_markdown_version: cache_version
+ )
+ end
let(:markdown) { '`Foo`' }
let(:html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Foo</code></p>' }
@@ -26,7 +33,7 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do
let(:updated_html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Bar</code></p>' }
context 'an unchanged markdown field' do
- let(:thing) { klass.new(title: markdown) }
+ let(:thing) { klass.new(project_id: project.id, namespace_id: project.project_namespace_id, title: markdown) }
before do
thing.title = thing.title
@@ -40,7 +47,12 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do
end
context 'a changed markdown field' do
- let(:thing) { klass.create!(title: markdown, title_html: html, cached_markdown_version: cache_version) }
+ let(:thing) do
+ klass.create!(
+ project_id: project.id, namespace_id: project.project_namespace_id,
+ title: markdown, title_html: html, cached_markdown_version: cache_version
+ )
+ end
before do
thing.title = updated_markdown
@@ -72,7 +84,12 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do
end
context 'a non-markdown field changed' do
- let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
+ let(:thing) do
+ klass.new(
+ project_id: project.id, namespace_id: project.project_namespace_id, title: markdown,
+ title_html: html, cached_markdown_version: cache_version
+ )
+ end
before do
thing.state_id = 2
@@ -86,7 +103,12 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do
end
context 'version is out of date' do
- let(:thing) { klass.new(title: updated_markdown, title_html: html, cached_markdown_version: nil) }
+ let(:thing) do
+ klass.new(
+ project_id: project.id, namespace_id: project.project_namespace_id,
+ title: updated_markdown, title_html: html, cached_markdown_version: nil
+ )
+ end
before do
thing.save!
@@ -127,7 +149,12 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do
end
describe '#cached_html_up_to_date?' do
- let(:thing) { klass.create!(title: updated_markdown, title_html: html, cached_markdown_version: nil) }
+ let(:thing) do
+ klass.create!(
+ project_id: project.id, namespace_id: project.project_namespace_id,
+ title: updated_markdown, title_html: html, cached_markdown_version: nil
+ )
+ end
subject { thing.cached_html_up_to_date?(:title) }
diff --git a/spec/lib/gitlab/memory/instrumentation_spec.rb b/spec/lib/gitlab/memory/instrumentation_spec.rb
index 069c45da18a..3d58f28ec1e 100644
--- a/spec/lib/gitlab/memory/instrumentation_spec.rb
+++ b/spec/lib/gitlab/memory/instrumentation_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Instrumentation do
+RSpec.describe Gitlab::Memory::Instrumentation, feature_category: :application_performance do
include MemoryInstrumentationHelper
before do
- skip_memory_instrumentation!
+ verify_memory_instrumentation_available!
end
describe '.available?' do
diff --git a/spec/lib/gitlab/memory/jemalloc_spec.rb b/spec/lib/gitlab/memory/jemalloc_spec.rb
index 414d6017534..8cce2278f8e 100644
--- a/spec/lib/gitlab/memory/jemalloc_spec.rb
+++ b/spec/lib/gitlab/memory/jemalloc_spec.rb
@@ -1,15 +1,14 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-require 'tmpdir'
+require 'tempfile'
RSpec.describe Gitlab::Memory::Jemalloc do
- let(:outdir) { Dir.mktmpdir }
- let(:tmp_outdir) { Dir.mktmpdir }
+ let(:outfile) { Tempfile.new }
after do
- FileUtils.rm_f(outdir)
- FileUtils.rm_f(tmp_outdir)
+ outfile.close
+ outfile.unlink
end
context 'when jemalloc is loaded' do
@@ -31,12 +30,11 @@ RSpec.describe Gitlab::Memory::Jemalloc do
describe '.dump_stats' do
it 'writes stats JSON file' do
- file_path = described_class.dump_stats(path: outdir, tmp_dir: tmp_outdir, format: format)
+ described_class.dump_stats(outfile, format: format)
- file = Dir.entries(outdir).find { |e| e.match(/jemalloc_stats\.#{$$}\.\d+\.json$/) }
- expect(file).not_to be_nil
- expect(file_path).to eq(File.join(outdir, file))
- expect(File.read(file_path)).to eq(output)
+ outfile.rewind
+
+ expect(outfile.read).to eq(output)
end
end
end
@@ -56,23 +54,12 @@ RSpec.describe Gitlab::Memory::Jemalloc do
end
describe '.dump_stats' do
- shared_examples 'writes stats text file' do |filename_label, filename_pattern|
- it do
- described_class.dump_stats(
- path: outdir, tmp_dir: tmp_outdir, format: format, filename_label: filename_label)
-
- file = Dir.entries(outdir).find { |e| e.match(filename_pattern) }
- expect(file).not_to be_nil
- expect(File.read(File.join(outdir, file))).to eq(output)
- end
- end
+ it 'writes stats text file' do
+ described_class.dump_stats(outfile, format: format)
- context 'when custom filename label is passed' do
- include_examples 'writes stats text file', 'puma_0', /jemalloc_stats\.#{$$}\.puma_0\.\d+\.txt$/
- end
+ outfile.rewind
- context 'when custom filename label is not passed' do
- include_examples 'writes stats text file', nil, /jemalloc_stats\.#{$$}\.\d+\.txt$/
+ expect(outfile.read).to eq(output)
end
end
end
@@ -91,7 +78,7 @@ RSpec.describe Gitlab::Memory::Jemalloc do
describe '.dump_stats' do
it 'raises an error' do
expect do
- described_class.dump_stats(path: outdir, tmp_dir: tmp_outdir, format: format)
+ described_class.dump_stats(outfile, format: format)
end.to raise_error(/format must be one of/)
end
end
@@ -104,18 +91,18 @@ RSpec.describe Gitlab::Memory::Jemalloc do
end
describe '.stats' do
- it 'returns nil' do
- expect(described_class.stats).to be_nil
+ it 'returns empty string' do
+ expect(described_class.stats).to be_empty
end
end
describe '.dump_stats' do
it 'does nothing' do
- stub_env('LD_PRELOAD', nil)
+ described_class.dump_stats(outfile)
- described_class.dump_stats(path: outdir, tmp_dir: tmp_outdir)
+ outfile.rewind
- expect(Dir.empty?(outdir)).to be(true)
+ expect(outfile.read).to be_empty
end
end
end
diff --git a/spec/lib/gitlab/memory/reporter_spec.rb b/spec/lib/gitlab/memory/reporter_spec.rb
new file mode 100644
index 00000000000..924397ceb4f
--- /dev/null
+++ b/spec/lib/gitlab/memory/reporter_spec.rb
@@ -0,0 +1,206 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Memory::Reporter, :aggregate_failures, feature_category: :application_performance do
+ let(:fake_report) do
+ Class.new do
+ def name
+ 'fake_report'
+ end
+
+ def active?
+ true
+ end
+
+ def run(writer)
+ writer << 'I ran'
+ end
+ end
+ end
+
+ let(:logger) { instance_double(::Logger) }
+ let(:report) { fake_report.new }
+
+ after do
+ FileUtils.rm_rf(reports_path)
+ end
+
+ describe '#run_report', time_travel_to: '2020-02-02 10:30:45 0000' do
+ let(:report_duration_counter) { instance_double(::Prometheus::Client::Counter) }
+ let(:file_size) { 1_000_000 }
+ let(:report_file) { "#{reports_path}/fake_report.2020-02-02.10:30:45:000.worker_1.abc123.gz" }
+
+ let(:input) { StringIO.new }
+ let(:output) { StringIO.new }
+
+ before do
+ allow(SecureRandom).to receive(:uuid).and_return('abc123')
+
+ allow(Gitlab::Metrics).to receive(:counter).and_return(report_duration_counter)
+ allow(report_duration_counter).to receive(:increment)
+
+ allow(::Prometheus::PidProvider).to receive(:worker_id).and_return('worker_1')
+ allow(File).to receive(:size).with(report_file).and_return(file_size)
+
+ allow(logger).to receive(:info)
+
+ stub_gzip
+ end
+
+ shared_examples 'runs and stores reports' do
+ it 'runs the given report and returns true' do
+ expect(reporter.run_report(report)).to be(true)
+
+ expect(output.string).to eq('I ran')
+ end
+
+ it 'closes read and write streams' do
+ expect(input).to receive(:close).ordered.at_least(:once)
+ expect(output).to receive(:close).ordered.at_least(:once)
+
+ reporter.run_report(report)
+ end
+
+ it 'logs start and finish event' do
+ expect(logger).to receive(:info).ordered.with(
+ hash_including(
+ message: 'started',
+ pid: Process.pid,
+ worker_id: 'worker_1',
+ perf_report_worker_uuid: 'abc123',
+ perf_report: 'fake_report'
+ ))
+ expect(logger).to receive(:info).ordered.with(
+ hash_including(
+ :duration_s,
+ :cpu_s,
+ perf_report_file: report_file,
+ perf_report_size_bytes: file_size,
+ message: 'finished',
+ pid: Process.pid,
+ worker_id: 'worker_1',
+ perf_report_worker_uuid: 'abc123',
+ perf_report: 'fake_report'
+ ))
+
+ reporter.run_report(report)
+ end
+
+ it 'increments Prometheus duration counter' do
+ expect(report_duration_counter).to receive(:increment).with({ report: 'fake_report' }, an_instance_of(Float))
+
+ reporter.run_report(report)
+ end
+
+ context 'when the report returns invalid file path' do
+ before do
+ allow(File).to receive(:size).with(report_file).and_raise(Errno::ENOENT)
+ end
+
+ it 'logs `0` as `perf_report_size_bytes`' do
+ expect(logger).to receive(:info).ordered.with(
+ hash_including(message: 'started')
+ )
+ expect(logger).to receive(:info).ordered.with(
+ hash_including(message: 'finished', perf_report_size_bytes: 0)
+ )
+
+ reporter.run_report(report)
+ end
+ end
+
+ context 'when an error occurs' do
+ before do
+ allow(report).to receive(:run).and_raise(RuntimeError.new('report failed'))
+ end
+
+ it 'logs the error and returns false' do
+ expect(logger).to receive(:info).ordered.with(hash_including(message: 'started'))
+ expect(logger).to receive(:error).ordered.with(
+ hash_including(
+ message: 'failed', error: '#<RuntimeError: report failed>'
+ ))
+
+ expect(reporter.run_report(report)).to be(false)
+ end
+
+ it 'closes read and write streams' do
+ allow(logger).to receive(:info)
+ allow(logger).to receive(:error)
+
+ expect(input).to receive(:close).ordered.at_least(:once)
+ expect(output).to receive(:close).ordered.at_least(:once)
+
+ reporter.run_report(report)
+ end
+
+ context 'when compression process is still running' do
+ it 'terminates the process' do
+ allow(logger).to receive(:info)
+ allow(logger).to receive(:error)
+
+ expect(Gitlab::ProcessManagement).to receive(:signal).with(an_instance_of(Integer), :KILL)
+
+ reporter.run_report(report)
+ end
+ end
+ end
+
+ context 'when a report is disabled' do
+ it 'does nothing and returns false' do
+ expect(report).to receive(:active?).and_return(false)
+ expect(report).not_to receive(:run)
+ expect(logger).not_to receive(:info)
+ expect(report_duration_counter).not_to receive(:increment)
+
+ reporter.run_report(report)
+ end
+ end
+ end
+
+ context 'when reports path is specified directly' do
+ let(:reports_path) { Dir.mktmpdir }
+
+ subject(:reporter) { described_class.new(reports_path: reports_path, logger: logger) }
+
+ it_behaves_like 'runs and stores reports'
+ end
+
+ context 'when reports path is specified via environment' do
+ let(:reports_path) { Dir.mktmpdir }
+
+ subject(:reporter) { described_class.new(logger: logger) }
+
+ before do
+ stub_env('GITLAB_DIAGNOSTIC_REPORTS_PATH', reports_path)
+ end
+
+ it_behaves_like 'runs and stores reports'
+ end
+
+ context 'when reports path is not specified' do
+ let(:reports_path) { reporter.reports_path }
+
+ subject(:reporter) { described_class.new(logger: logger) }
+
+ it 'defaults to a temporary location' do
+ expect(reports_path).not_to be_empty
+ end
+
+ it_behaves_like 'runs and stores reports'
+ end
+ end
+
+ # We need to stub out the call into gzip. We do this by intercepting the write
+ # end of the pipe and replacing it with a StringIO instead, which we can
+ # easily inspect for contents.
+ def stub_gzip
+ pid = 42
+ allow(IO).to receive(:pipe).and_return([input, output])
+ allow(Process).to receive(:spawn).with(
+ "gzip", "--fast", in: input, out: an_instance_of(File), err: an_instance_of(IO)
+ ).and_return(pid)
+ allow(Process).to receive(:waitpid).with(pid)
+ end
+end
diff --git a/spec/lib/gitlab/memory/reports/heap_dump_spec.rb b/spec/lib/gitlab/memory/reports/heap_dump_spec.rb
new file mode 100644
index 00000000000..4e235a71bdb
--- /dev/null
+++ b/spec/lib/gitlab/memory/reports/heap_dump_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Memory::Reports::HeapDump, feature_category: :application_performance do
+ # Copy this class so we do not mess with its state.
+ let(:klass) { described_class.dup }
+
+ subject(:report) { klass.new }
+
+ describe '#name' do
+ # This is a bit silly, but it caused code coverage failures.
+ it 'is set' do
+ expect(report.name).to eq('heap_dump')
+ end
+ end
+
+ describe '#active?' do
+ it 'is true when report_heap_dumps is enabled' do
+ expect(report).to be_active
+ end
+
+ it 'is false when report_heap_dumps is disabled' do
+ stub_feature_flags(report_heap_dumps: false)
+
+ expect(report).not_to be_active
+ end
+ end
+
+ describe '#run' do
+ subject(:run) { report.run(writer) }
+
+ let(:writer) { StringIO.new }
+
+ context 'when no heap dump is enqueued' do
+ it 'does nothing and returns false' do
+ expect(ObjectSpace).not_to receive(:dump_all)
+
+ expect(run).to be(false)
+ end
+ end
+
+ context 'when a heap dump is enqueued', :aggregate_failures do
+ it 'dumps heap and returns true' do
+ expect(ObjectSpace).to receive(:dump_all).with(output: writer) do |output:|
+ output << 'heap contents'
+ end
+
+ klass.enqueue!
+
+ expect(run).to be(true)
+ expect(writer.string).to eq('heap contents')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb b/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb
index b327a40bc2c..ce06c270a05 100644
--- a/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb
+++ b/spec/lib/gitlab/memory/reports/jemalloc_stats_spec.rb
@@ -3,96 +3,16 @@
require 'spec_helper'
RSpec.describe Gitlab::Memory::Reports::JemallocStats do
- let_it_be(:outdir) { Dir.mktmpdir }
+ subject(:jemalloc_stats) { described_class.new }
- let(:jemalloc_stats) { described_class.new(reports_path: outdir) }
-
- after do
- FileUtils.rm_f(outdir)
- end
+ let(:writer) { StringIO.new }
describe '.run' do
context 'when :report_jemalloc_stats ops FF is enabled' do
- let(:worker_id) { 'puma_1' }
- let(:report_name) { 'report.json' }
- let(:report_path) { File.join(outdir, report_name) }
-
- before do
- allow(Prometheus::PidProvider).to receive(:worker_id).and_return(worker_id)
- end
-
- it 'invokes Jemalloc.dump_stats and returns file path' do
- expect(Gitlab::Memory::Jemalloc)
- .to receive(:dump_stats)
- .with(path: outdir,
- tmp_dir: File.join(outdir, '/tmp'),
- filename_label: worker_id)
- .and_return(report_path)
-
- expect(jemalloc_stats.run).to eq(report_path)
- end
-
- describe 'reports cleanup' do
- let(:jemalloc_stats) { described_class.new(reports_path: outdir) }
-
- before do
- stub_env('GITLAB_DIAGNOSTIC_REPORTS_JEMALLOC_MAX_REPORTS_STORED', 3)
- allow(Gitlab::Memory::Jemalloc).to receive(:dump_stats)
- end
-
- context 'when number of reports exceeds `max_reports_stored`' do
- let_it_be(:reports) do
- now = Time.current
-
- (1..5).map do |i|
- Tempfile.new("jemalloc_stats.#{i}.worker_#{i}.#{Time.current.to_i}.json", outdir).tap do |f|
- FileUtils.touch(f, mtime: (now + i.second).to_i)
- end
- end
- end
-
- after do
- reports.each do |f|
- f.close
- f.unlink
- rescue Errno::ENOENT
- # Some of the files are already unlinked by the code we test; Ignore
- end
- end
-
- it 'keeps only `max_reports_stored` total newest files' do
- expect { jemalloc_stats.run }
- .to change { Dir.entries(outdir).count { |e| e.match(/jemalloc_stats.*/) } }
- .from(5).to(3)
-
- # Keeps only the newest reports
- expect(reports.last(3).all? { |r| File.exist?(r) }).to be true
- end
- end
-
- context 'when number of reports does not exceed `max_reports_stored`' do
- let_it_be(:reports) do
- now = Time.current
-
- (1..3).map do |i|
- Tempfile.new("jemalloc_stats.#{i}.worker_#{i}.#{Time.current.to_i}.json", outdir).tap do |f|
- FileUtils.touch(f, mtime: (now + i.second).to_i)
- end
- end
- end
-
- after do
- reports.each do |f|
- f.close
- f.unlink
- end
- end
+ it 'dumps jemalloc stats to the given writer' do
+ expect(Gitlab::Memory::Jemalloc).to receive(:dump_stats).with(writer)
- it 'does not remove any reports' do
- expect { jemalloc_stats.run }
- .not_to change { Dir.entries(outdir).count { |e| e.match(/jemalloc_stats.*/) } }
- end
- end
+ jemalloc_stats.run(writer)
end
end
@@ -101,10 +21,10 @@ RSpec.describe Gitlab::Memory::Reports::JemallocStats do
stub_feature_flags(report_jemalloc_stats: false)
end
- it 'does not run the report and returns nil' do
+ it 'does not run the report' do
expect(Gitlab::Memory::Jemalloc).not_to receive(:dump_stats)
- expect(jemalloc_stats.run).to be_nil
+ jemalloc_stats.run(writer)
end
end
end
diff --git a/spec/lib/gitlab/memory/reports_daemon_spec.rb b/spec/lib/gitlab/memory/reports_daemon_spec.rb
index 0473e170502..91c36c87253 100644
--- a/spec/lib/gitlab/memory/reports_daemon_spec.rb
+++ b/spec/lib/gitlab/memory/reports_daemon_spec.rb
@@ -3,94 +3,58 @@
require 'spec_helper'
RSpec.describe Gitlab::Memory::ReportsDaemon, :aggregate_failures do
- let(:daemon) { described_class.new }
+ let(:reporter) { instance_double(Gitlab::Memory::Reporter) }
+ let(:reports) { nil }
- let_it_be(:tmp_dir) { Dir.mktmpdir }
-
- after(:all) do
- FileUtils.remove_entry(tmp_dir)
- end
+ subject(:daemon) { described_class.new(reporter: reporter, reports: reports) }
describe '#run_thread' do
- let(:report_duration_counter) { instance_double(::Prometheus::Client::Counter) }
- let(:file_size) { 1_000_000 }
-
before do
- allow(Gitlab::Metrics).to receive(:counter).and_return(report_duration_counter)
- allow(report_duration_counter).to receive(:increment)
-
# make sleep no-op
allow(daemon).to receive(:sleep) {}
# let alive return 3 times: true, true, false
allow(daemon).to receive(:alive).and_return(true, true, false)
-
- allow(File).to receive(:size).with(/#{daemon.reports_path}.*\.json/).and_return(file_size)
- end
-
- it 'runs reports, logs and sets gauge' do
- expect(daemon.send(:reports))
- .to all(receive(:run).twice { Tempfile.new("report.json", tmp_dir).path })
-
- expect(::Prometheus::PidProvider).to receive(:worker_id).at_least(:once).and_return('worker_1')
-
- expect(Gitlab::AppLogger).to receive(:info).with(
- hash_including(
- :duration_s,
- :cpu_s,
- perf_report_size_bytes: file_size,
- message: 'finished',
- pid: Process.pid,
- worker_id: 'worker_1',
- perf_report: 'jemalloc_stats'
- )).twice
-
- expect(report_duration_counter).to receive(:increment).with({ report: 'jemalloc_stats' }, an_instance_of(Float))
-
- daemon.send(:run_thread)
end
- context 'when the report object returns invalid file path' do
- before do
- allow(File).to receive(:size).with(/#{daemon.reports_path}.*\.json/).and_raise(Errno::ENOENT)
- end
-
- it 'logs `0` as `perf_report_size_bytes`' do
- expect(daemon.send(:reports))
- .to all(receive(:run).twice { Tempfile.new("report.json", tmp_dir).path })
-
- expect(Gitlab::AppLogger).to receive(:info).with(hash_including(perf_report_size_bytes: 0)).twice
+ context 'with default reports' do
+ it 'runs them using the given reporter' do
+ expect(reporter).to receive(:run_report).twice.with(an_instance_of(Gitlab::Memory::Reports::JemallocStats))
daemon.send(:run_thread)
end
end
- it 'allows configure and run multiple reports' do
+ context 'with inactive reports' do
# rubocop: disable RSpec/VerifiedDoubles
# We test how ReportsDaemon could be extended in the future
# We configure it with new reports classes which are not yet defined so we cannot make this an instance_double.
- active_report_1 = double("Active Report 1", active?: true)
- active_report_2 = double("Active Report 2", active?: true)
- inactive_report = double("Inactive Report", active?: false)
+ let(:active_report_1) { double("Active Report 1", active?: true) }
+ let(:active_report_2) { double("Active Report 2", active?: true) }
+ let(:inactive_report) { double("Inactive Report", active?: false) }
# rubocop: enable RSpec/VerifiedDoubles
- allow(daemon).to receive(:reports).and_return([active_report_1, inactive_report, active_report_2])
+ let(:reports) do
+ [active_report_1, active_report_2, inactive_report]
+ end
- expect(active_report_1).to receive(:run).and_return(File.join(tmp_dir, 'report_1.json')).twice
- expect(active_report_2).to receive(:run).and_return(File.join(tmp_dir, 'report_2.json')).twice
- expect(inactive_report).not_to receive(:run)
+ it 'runs only active reports' do
+ expect(reporter).to receive(:run_report).ordered.with(active_report_1)
+ expect(reporter).to receive(:run_report).ordered.with(active_report_2)
+ expect(reporter).to receive(:run_report).ordered.with(active_report_1)
+ expect(reporter).to receive(:run_report).ordered.with(active_report_2)
+ expect(reporter).not_to receive(:run_report).with(inactive_report)
- daemon.send(:run_thread)
+ daemon.send(:run_thread)
+ end
end
context 'sleep timers logic' do
it 'wakes up every (fixed interval + defined delta), sleeps between reports each cycle' do
stub_env('GITLAB_DIAGNOSTIC_REPORTS_SLEEP_MAX_DELTA_S', 1) # rand(1) == 0, so we will have fixed sleep interval
- daemon = described_class.new
+ daemon = described_class.new(reporter: reporter, reports: reports)
allow(daemon).to receive(:alive).and_return(true, true, false)
-
- expect(daemon.send(:reports))
- .to all(receive(:run).twice { Tempfile.new("report.json", tmp_dir).path })
+ allow(reporter).to receive(:run_report)
expect(daemon).to receive(:sleep).with(described_class::DEFAULT_SLEEP_S).ordered
expect(daemon).to receive(:sleep).with(described_class::DEFAULT_SLEEP_BETWEEN_REPORTS_S).ordered
@@ -116,7 +80,6 @@ RSpec.describe Gitlab::Memory::ReportsDaemon, :aggregate_failures do
expect(daemon.sleep_s).to eq(described_class::DEFAULT_SLEEP_S)
expect(daemon.sleep_max_delta_s).to eq(described_class::DEFAULT_SLEEP_MAX_DELTA_S)
expect(daemon.sleep_between_reports_s).to eq(described_class::DEFAULT_SLEEP_BETWEEN_REPORTS_S)
- expect(daemon.reports_path).to eq(described_class::DEFAULT_REPORTS_PATH)
end
end
@@ -125,7 +88,6 @@ RSpec.describe Gitlab::Memory::ReportsDaemon, :aggregate_failures do
stub_env('GITLAB_DIAGNOSTIC_REPORTS_SLEEP_S', 100)
stub_env('GITLAB_DIAGNOSTIC_REPORTS_SLEEP_MAX_DELTA_S', 50)
stub_env('GITLAB_DIAGNOSTIC_REPORTS_SLEEP_BETWEEN_REPORTS_S', 2)
- stub_env('GITLAB_DIAGNOSTIC_REPORTS_PATH', tmp_dir)
end
it 'uses provided values' do
@@ -134,7 +96,6 @@ RSpec.describe Gitlab::Memory::ReportsDaemon, :aggregate_failures do
expect(daemon.sleep_s).to eq(100)
expect(daemon.sleep_max_delta_s).to eq(50)
expect(daemon.sleep_between_reports_s).to eq(2)
- expect(daemon.reports_path).to eq(tmp_dir)
end
end
end
diff --git a/spec/lib/gitlab/memory/watchdog/configuration_spec.rb b/spec/lib/gitlab/memory/watchdog/configuration_spec.rb
index 38a39f6a33a..9242344ead2 100644
--- a/spec/lib/gitlab/memory/watchdog/configuration_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/configuration_spec.rb
@@ -20,10 +20,14 @@ RSpec.describe Gitlab::Memory::Watchdog::Configuration do
end
end
- describe '#logger' do
- context 'when logger is not set, defaults to stdout logger' do
- it 'defaults to Logger' do
- expect(configuration.logger).to be_an_instance_of(::Gitlab::Logger)
+ describe '#event_reporter' do
+ context 'when event reporter is not set' do
+ before do
+ allow(Gitlab::Metrics).to receive(:counter)
+ end
+
+ it 'defaults to EventReporter' do
+ expect(configuration.event_reporter).to be_an_instance_of(::Gitlab::Memory::Watchdog::EventReporter)
end
end
end
@@ -38,6 +42,8 @@ RSpec.describe Gitlab::Memory::Watchdog::Configuration do
describe '#monitors' do
context 'when monitors are configured to be used' do
+ let(:monitor_name1) { :monitor1 }
+ let(:monitor_name2) { :monitor2 }
let(:payload1) do
{
message: 'monitor_1_text',
@@ -96,7 +102,17 @@ RSpec.describe Gitlab::Memory::Watchdog::Configuration do
expect(payloads).to eq([payload1, payload2])
expect(thresholds).to eq([false, true])
expect(strikes).to eq([false, true])
- expect(monitor_names).to eq([:monitor1, :monitor2])
+ expect(monitor_names).to eq([monitor_name1, monitor_name2])
+ end
+
+ it 'monitors are not empty' do
+ expect(configuration.monitors).not_to be_empty
+ end
+ end
+
+ context 'when monitors are not configured' do
+ it 'monitors are empty' do
+ expect(configuration.monitors).to be_empty
end
end
@@ -119,18 +135,19 @@ RSpec.describe Gitlab::Memory::Watchdog::Configuration do
include_examples 'executes monitors and returns correct results'
end
- end
- context 'when same monitor class is configured twice' do
- before do
- configuration.monitors.push monitor_class_1, max_strikes: 1
- configuration.monitors.push monitor_class_1, max_strikes: 1
- end
+ context 'when monitors are configured with monitor name' do
+ let(:monitor_name1) { :mon_one }
+ let(:monitor_name2) { :mon_two }
- it 'calls same monitor only once' do
- expect do |b|
- configuration.monitors.call_each(&b)
- end.to yield_control.once
+ before do
+ configuration.monitors do |stack|
+ stack.push monitor_class_1, false, { message: 'monitor_1_text' }, max_strikes: 5, monitor_name: :mon_one
+ stack.push monitor_class_2, true, { message: 'monitor_2_text' }, max_strikes: 0, monitor_name: :mon_two
+ end
+ end
+
+ include_examples 'executes monitors and returns correct results'
end
end
end
diff --git a/spec/lib/gitlab/memory/watchdog/configurator_spec.rb b/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
index 86cbb724cfd..a901be84a21 100644
--- a/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
@@ -6,17 +6,23 @@ require 'sidekiq'
require_dependency 'gitlab/cluster/lifecycle_events'
RSpec.describe Gitlab::Memory::Watchdog::Configurator do
- shared_examples 'as configurator' do |handler_class, sleep_time_env, sleep_time|
+ shared_examples 'as configurator' do |handler_class, event_reporter_class, sleep_time_env, sleep_time|
it 'configures the correct handler' do
configurator.call(configuration)
expect(configuration.handler).to be_an_instance_of(handler_class)
end
+ it 'configures the correct event reporter' do
+ configurator.call(configuration)
+
+ expect(configuration.event_reporter).to be_an_instance_of(event_reporter_class)
+ end
+
it 'configures the correct logger' do
configurator.call(configuration)
- expect(configuration.logger).to eq(logger)
+ expect(configuration.event_reporter.logger).to eq(logger)
end
context 'when sleep_time_seconds is not passed through the environment' do
@@ -87,12 +93,13 @@ RSpec.describe Gitlab::Memory::Watchdog::Configurator do
it_behaves_like 'as configurator',
Gitlab::Memory::Watchdog::PumaHandler,
+ Gitlab::Memory::Watchdog::EventReporter,
'GITLAB_MEMWD_SLEEP_TIME_SEC',
- 60
+ described_class::DEFAULT_SLEEP_INTERVAL_S
context 'with DISABLE_PUMA_WORKER_KILLER set to true' do
- let(:primary_memory) { 2048 }
- let(:worker_memory) { max_mem_growth * primary_memory + 1 }
+ let(:primary_memory_bytes) { 2_097_152_000 }
+ let(:worker_memory_bytes) { max_mem_growth * primary_memory_bytes + 1 }
let(:expected_payloads) do
{
heap_fragmentation: {
@@ -105,9 +112,9 @@ RSpec.describe Gitlab::Memory::Watchdog::Configurator do
},
unique_memory_growth: {
message: 'memory limit exceeded',
- memwd_uss_bytes: worker_memory,
- memwd_ref_uss_bytes: primary_memory,
- memwd_max_uss_bytes: max_mem_growth * primary_memory,
+ memwd_uss_bytes: worker_memory_bytes,
+ memwd_ref_uss_bytes: primary_memory_bytes,
+ memwd_max_uss_bytes: max_mem_growth * primary_memory_bytes,
memwd_max_strikes: max_strikes,
memwd_cur_strikes: 1
}
@@ -117,10 +124,10 @@ RSpec.describe Gitlab::Memory::Watchdog::Configurator do
before do
stub_env('DISABLE_PUMA_WORKER_KILLER', true)
allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(max_heap_fragmentation + 0.1)
- allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return({ uss: worker_memory })
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return({ uss: worker_memory_bytes })
allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).with(
pid: Gitlab::Cluster::PRIMARY_PID
- ).and_return({ uss: primary_memory })
+ ).and_return({ uss: primary_memory_bytes })
end
context 'when settings are set via environment variables' do
@@ -138,21 +145,22 @@ RSpec.describe Gitlab::Memory::Watchdog::Configurator do
end
context 'when settings are not set via environment variables' do
- let(:max_heap_fragmentation) { 0.5 }
- let(:max_mem_growth) { 3.0 }
- let(:max_strikes) { 5 }
+ let(:max_heap_fragmentation) { described_class::DEFAULT_MAX_HEAP_FRAG }
+ let(:max_mem_growth) { described_class::DEFAULT_MAX_MEM_GROWTH }
+ let(:max_strikes) { described_class::DEFAULT_MAX_STRIKES }
it_behaves_like 'as monitor configurator'
end
end
context 'with DISABLE_PUMA_WORKER_KILLER set to false' do
+ let(:memory_limit_bytes) { memory_limit_mb.megabytes }
let(:expected_payloads) do
{
rss_memory_limit: {
message: 'rss memory limit exceeded',
- memwd_rss_bytes: memory_limit + 1,
- memwd_max_rss_bytes: memory_limit,
+ memwd_rss_bytes: memory_limit_bytes + 1,
+ memwd_max_rss_bytes: memory_limit_bytes,
memwd_max_strikes: max_strikes,
memwd_cur_strikes: 1
}
@@ -161,15 +169,15 @@ RSpec.describe Gitlab::Memory::Watchdog::Configurator do
before do
stub_env('DISABLE_PUMA_WORKER_KILLER', false)
- allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ total: memory_limit + 1 })
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ total: memory_limit_bytes + 1 })
end
context 'when settings are set via environment variables' do
- let(:memory_limit) { 1300.megabytes }
+ let(:memory_limit_mb) { 1300 }
let(:max_strikes) { 4 }
before do
- stub_env('PUMA_WORKER_MAX_MEMORY', 1300)
+ stub_env('PUMA_WORKER_MAX_MEMORY', memory_limit_mb)
stub_env('GITLAB_MEMWD_MAX_STRIKES', 4)
end
@@ -177,8 +185,8 @@ RSpec.describe Gitlab::Memory::Watchdog::Configurator do
end
context 'when settings are not set via environment variables' do
- let(:memory_limit) { 1200.megabytes }
- let(:max_strikes) { 5 }
+ let(:memory_limit_mb) { described_class::DEFAULT_PUMA_WORKER_RSS_LIMIT_MB }
+ let(:max_strikes) { described_class::DEFAULT_MAX_STRIKES }
it_behaves_like 'as monitor configurator'
end
@@ -193,7 +201,115 @@ RSpec.describe Gitlab::Memory::Watchdog::Configurator do
it_behaves_like 'as configurator',
Gitlab::Memory::Watchdog::TermProcessHandler,
+ Gitlab::Memory::Watchdog::SidekiqEventReporter,
'SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL',
- 3
+ described_class::DEFAULT_SIDEKIQ_SLEEP_INTERVAL_S
+
+ context 'when sleep_time_seconds is less than MIN_SIDEKIQ_SLEEP_INTERVAL_S seconds' do
+ before do
+ stub_env('SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL', 0)
+ end
+
+ it 'configures the correct sleep time' do
+ configurator.call(configuration)
+
+ expect(configuration.sleep_time_seconds).to eq(described_class::MIN_SIDEKIQ_SLEEP_INTERVAL_S)
+ end
+ end
+
+ context 'with monitors' do
+ let(:soft_limit_bytes) { soft_limit_kb.kilobytes }
+ let(:hard_limit_bytes) { hard_limit_kb.kilobytes }
+
+ context 'when settings are set via environment variables' do
+ let(:soft_limit_kb) { 2000001 }
+ let(:hard_limit_kb) { 300000 }
+ let(:max_strikes) { 150 }
+ let(:grace_time) { 300 }
+ let(:expected_payloads) do
+ {
+ rss_memory_soft_limit: {
+ message: 'rss memory limit exceeded',
+ memwd_rss_bytes: soft_limit_bytes + 1,
+ memwd_max_rss_bytes: soft_limit_bytes,
+ memwd_max_strikes: max_strikes,
+ memwd_cur_strikes: 1
+ },
+ rss_memory_hard_limit: {
+ message: 'rss memory limit exceeded',
+ memwd_rss_bytes: hard_limit_bytes + 1,
+ memwd_max_rss_bytes: hard_limit_bytes,
+ memwd_max_strikes: 0,
+ memwd_cur_strikes: 1
+ }
+ }
+ end
+
+ before do
+ stub_env('SIDEKIQ_MEMORY_KILLER_MAX_RSS', soft_limit_kb)
+ stub_env('SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS', hard_limit_kb)
+ stub_env('SIDEKIQ_MEMORY_KILLER_GRACE_TIME', grace_time)
+ stub_env('SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL', 2)
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_rss)
+ .and_return({ total: soft_limit_bytes + 1 }, { total: hard_limit_bytes + 1 })
+ end
+
+ it_behaves_like 'as monitor configurator'
+ end
+
+ context 'when only SIDEKIQ_MEMORY_KILLER_MAX_RSS is set via environment variable' do
+ let(:soft_limit_kb) { 2000000 }
+ let(:max_strikes) do
+ described_class::DEFAULT_SIDEKIQ_GRACE_TIME_S / described_class::DEFAULT_SIDEKIQ_SLEEP_INTERVAL_S
+ end
+
+ let(:expected_payloads) do
+ {
+ rss_memory_soft_limit: {
+ message: 'rss memory limit exceeded',
+ memwd_rss_bytes: soft_limit_bytes + 1,
+ memwd_max_rss_bytes: soft_limit_bytes,
+ memwd_max_strikes: max_strikes,
+ memwd_cur_strikes: 1
+ }
+ }
+ end
+
+ before do
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ total: soft_limit_bytes + 1 })
+ stub_env('SIDEKIQ_MEMORY_KILLER_MAX_RSS', soft_limit_kb)
+ end
+
+ it_behaves_like 'as monitor configurator'
+ end
+
+ context 'when only SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS is set via environment variable' do
+ let(:hard_limit_kb) { 2000000 }
+ let(:expected_payloads) do
+ {
+ rss_memory_hard_limit: {
+ message: 'rss memory limit exceeded',
+ memwd_rss_bytes: hard_limit_bytes + 1,
+ memwd_max_rss_bytes: hard_limit_bytes,
+ memwd_max_strikes: 0,
+ memwd_cur_strikes: 1
+ }
+ }
+ end
+
+ before do
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ total: hard_limit_bytes + 1 })
+ stub_env('SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS', hard_limit_kb)
+ end
+
+ it_behaves_like 'as monitor configurator'
+ end
+
+ context 'when both SIDEKIQ_MEMORY_KILLER_MAX_RSS and SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS are not set' do
+ let(:expected_payloads) { {} }
+
+ it_behaves_like 'as monitor configurator'
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb b/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb
new file mode 100644
index 00000000000..f667bc724d2
--- /dev/null
+++ b/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'prometheus/client'
+
+RSpec.describe Gitlab::Memory::Watchdog::EventReporter, feature_category: :application_performance do
+ let(:logger) { instance_double(::Logger) }
+ let(:violations_counter) { instance_double(::Prometheus::Client::Counter) }
+ let(:violations_handled_counter) { instance_double(::Prometheus::Client::Counter) }
+ let(:reporter) { described_class.new(logger: logger) }
+
+ def stub_prometheus_metrics
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(:gitlab_memwd_violations_total, anything, anything)
+ .and_return(violations_counter)
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(:gitlab_memwd_violations_handled_total, anything, anything)
+ .and_return(violations_handled_counter)
+
+ allow(violations_counter).to receive(:increment)
+ allow(violations_handled_counter).to receive(:increment)
+ end
+
+ before do
+ stub_prometheus_metrics
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(
+ total: 1024
+ )
+ allow(::Prometheus::PidProvider).to receive(:worker_id).and_return('worker_1')
+ end
+
+ describe '#logger' do
+ context 'when logger is not provided' do
+ let(:reporter) { described_class.new }
+
+ it 'uses default Gitlab::AppLogger' do
+ expect(reporter.logger).to eq(Gitlab::AppLogger)
+ end
+ end
+ end
+
+ describe '#started' do
+ it 'logs start message once' do
+ expect(logger).to receive(:info).once
+ .with(
+ pid: Process.pid,
+ worker_id: 'worker_1',
+ custom_label: 'dummy_label',
+ memwd_rss_bytes: 1024,
+ message: 'started')
+
+ reporter.started(custom_label: 'dummy_label')
+ end
+ end
+
+ describe '#stopped' do
+ subject { reporter.stopped(custom_label: 'dummy_label') }
+
+ it 'logs stop message once' do
+ expect(logger).to receive(:info).once
+ .with(
+ pid: Process.pid,
+ worker_id: 'worker_1',
+ custom_label: 'dummy_label',
+ memwd_rss_bytes: 1024,
+ message: 'stopped')
+
+ reporter.stopped(custom_label: 'dummy_label')
+ end
+ end
+
+ describe '#threshold_violated' do
+ subject { reporter.threshold_violated(:monitor_name) }
+
+ it 'increments violations counter' do
+ expect(violations_counter).to receive(:increment).with(reason: :monitor_name)
+
+ subject
+ end
+
+ it 'does not increment handled violations counter' do
+ expect(violations_handled_counter).not_to receive(:increment)
+
+ subject
+ end
+
+ it 'does not log violation' do
+ expect(logger).not_to receive(:warn)
+
+ subject
+ end
+ end
+
+ describe '#strikes_exceeded' do
+ subject { reporter.strikes_exceeded(:monitor_name, { message: 'dummy_text' }) }
+
+ before do
+ allow(logger).to receive(:warn)
+ end
+
+ it 'increments handled violations counter' do
+ expect(violations_handled_counter).to receive(:increment).with(reason: :monitor_name)
+
+ subject
+ end
+
+ it 'logs violation' do
+ expect(logger).to receive(:warn)
+ .with(
+ pid: Process.pid,
+ worker_id: 'worker_1',
+ memwd_rss_bytes: 1024,
+ message: 'dummy_text')
+
+ subject
+ end
+ end
+end
diff --git a/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb b/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb
index 9e25cfda782..4780b1eba53 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,25 +4,38 @@ require 'fast_spec_helper'
require 'support/shared_examples/lib/gitlab/memory/watchdog/monitor_result_shared_examples'
RSpec.describe Gitlab::Memory::Watchdog::Monitor::RssMemoryLimit do
- let(:memory_limit) { 2048 }
- let(:worker_memory) { 1024 }
+ 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 }
subject(:monitor) do
- described_class.new(memory_limit: memory_limit)
+ described_class.new(memory_limit_bytes: memory_limit_bytes)
end
before do
- allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ total: worker_memory })
+ allow(Gitlab::Metrics).to receive(:gauge)
+ .with(:gitlab_memwd_max_memory_limit, anything)
+ .and_return(max_rss_limit_gauge)
+ allow(max_rss_limit_gauge).to receive(:set)
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ total: worker_memory_bytes })
+ end
+
+ describe '#initialize' do
+ it 'sets the max rss limit gauge' do
+ expect(max_rss_limit_gauge).to receive(:set).with({}, memory_limit_bytes)
+
+ monitor
+ end
end
describe '#call' do
context 'when process exceeds threshold' do
- let(:worker_memory) { memory_limit + 1 }
+ let(:worker_memory_bytes) { memory_limit_bytes + 1 }
let(:payload) do
{
message: 'rss memory limit exceeded',
- memwd_rss_bytes: worker_memory,
- memwd_max_rss_bytes: memory_limit
+ memwd_rss_bytes: worker_memory_bytes,
+ memwd_max_rss_bytes: memory_limit_bytes
}
end
@@ -30,7 +43,7 @@ RSpec.describe Gitlab::Memory::Watchdog::Monitor::RssMemoryLimit do
end
context 'when process does not exceed threshold' do
- let(:worker_memory) { memory_limit - 1 }
+ let(:worker_memory_bytes) { memory_limit_bytes - 1 }
let(:payload) { {} }
include_examples 'returns Watchdog Monitor result', threshold_violated: false
diff --git a/spec/lib/gitlab/memory/watchdog/monitor_state_spec.rb b/spec/lib/gitlab/memory/watchdog/monitor_state_spec.rb
index ace1353c6e3..7802e274c53 100644
--- a/spec/lib/gitlab/memory/watchdog/monitor_state_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/monitor_state_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Gitlab::Memory::Watchdog::MonitorState do
let(:payload) { { message: 'DummyMessage' } }
let(:threshold_violated) { true }
let(:monitor) { monitor_class.new(threshold_violated, payload) }
+ let(:monitor_name) { :dummy_monitor_name }
let(:monitor_class) do
Struct.new(:threshold_violated, :payload) do
def call
@@ -19,7 +20,7 @@ RSpec.describe Gitlab::Memory::Watchdog::MonitorState do
end
end
- subject(:monitor_state) { described_class.new(monitor, max_strikes: max_strikes) }
+ subject(:monitor_state) { described_class.new(monitor, max_strikes: max_strikes, monitor_name: monitor_name) }
shared_examples 'returns correct result' do
it 'returns correct result', :aggregate_failures do
@@ -29,7 +30,7 @@ RSpec.describe Gitlab::Memory::Watchdog::MonitorState do
expect(result.strikes_exceeded?).to eq(strikes_exceeded)
expect(result.threshold_violated?).to eq(threshold_violated)
expect(result.payload).to eq(expected_payload)
- expect(result.monitor_name).to eq(:monitor_name)
+ expect(result.monitor_name).to eq(monitor_name)
end
end
@@ -63,10 +64,4 @@ RSpec.describe Gitlab::Memory::Watchdog::MonitorState do
end
end
end
-
- describe '#monitor_class' do
- subject { monitor_state.monitor_class }
-
- it { is_expected.to eq(monitor_class) }
- end
end
diff --git a/spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb b/spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb
new file mode 100644
index 00000000000..48595c3f172
--- /dev/null
+++ b/spec/lib/gitlab/memory/watchdog/sidekiq_event_reporter_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Memory::Watchdog::SidekiqEventReporter, feature_category: :application_performance do
+ let(:counter) { instance_double(::Prometheus::Client::Counter) }
+
+ before do
+ allow(Gitlab::Metrics).to receive(:counter).and_return(counter)
+ allow(counter).to receive(:increment)
+ end
+
+ describe 'delegations' do
+ it { is_expected.to delegate_method(:started).to(:event_reporter) }
+ it { is_expected.to delegate_method(:stopped).to(:event_reporter) }
+ it { is_expected.to delegate_method(:threshold_violated).to(:event_reporter) }
+ it { is_expected.to delegate_method(:logger).to(:event_reporter) }
+ end
+
+ describe '#strikes_exceeded' do
+ let(:sidekiq_event_reporter) { described_class.new(logger: logger) }
+ let(:sidekiq_watchdog_running_jobs_counter) { instance_double(::Prometheus::Client::Counter) }
+ let(:logger) { instance_double(::Logger) }
+ let(:queue) { 'default' }
+ let(:jid) { SecureRandom.hex }
+ let(:running_jobs) { { jid => { worker_class: DummyWorker } } }
+ let(:sidekiq_daemon_monitor) { instance_double(Gitlab::SidekiqDaemon::Monitor) }
+ let(:worker) do
+ Class.new do
+ def self.name
+ 'DummyWorker'
+ end
+ end
+ end
+
+ before do
+ stub_const('DummyWorker', worker)
+ allow(Gitlab::SidekiqDaemon::Monitor).to receive(:instance).and_return(sidekiq_daemon_monitor)
+ allow(::Gitlab::Metrics).to receive(:counter)
+ .with(:sidekiq_watchdog_running_jobs_total, anything)
+ .and_return(sidekiq_watchdog_running_jobs_counter)
+ allow(sidekiq_watchdog_running_jobs_counter).to receive(:increment)
+ allow(logger).to receive(:warn)
+
+ allow(sidekiq_daemon_monitor).to receive(:jobs).and_return(running_jobs)
+ end
+
+ it 'delegates #strikes_exceeded with correct arguments' do
+ is_expected.to delegate_method(:strikes_exceeded).to(:event_reporter)
+ .with_arguments(
+ :monitor_name,
+ {
+ message: 'dummy_text',
+ running_jobs: [jid: jid, worker_class: 'DummyWorker']
+ }
+ )
+ end
+
+ it 'increment running jobs counter' do
+ expect(sidekiq_watchdog_running_jobs_counter).to receive(:increment)
+ .with({ worker_class: "DummyWorker" })
+
+ sidekiq_event_reporter.strikes_exceeded(:monitor_name, { message: 'dummy_text' })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb
index 5d9599d6eab..668ea36d420 100644
--- a/spec/lib/gitlab/memory/watchdog_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog_spec.rb
@@ -2,15 +2,13 @@
require 'spec_helper'
-RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures do
+RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category: :application_performance do
context 'watchdog' do
let(:configuration) { instance_double(described_class::Configuration) }
let(:handler) { instance_double(described_class::NullHandler) }
- let(:logger) { instance_double(::Logger) }
+ let(:reporter) { instance_double(described_class::EventReporter) }
let(:sleep_time_seconds) { 60 }
let(:threshold_violated) { false }
- let(:violations_counter) { instance_double(::Prometheus::Client::Counter) }
- let(:violations_handled_counter) { instance_double(::Prometheus::Client::Counter) }
let(:watchdog_iterations) { 1 }
let(:name) { :monitor_name }
let(:payload) { { message: 'dummy_text' } }
@@ -37,18 +35,6 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures do
end
end
- def stub_prometheus_metrics
- allow(Gitlab::Metrics).to receive(:counter)
- .with(:gitlab_memwd_violations_total, anything, anything)
- .and_return(violations_counter)
- allow(Gitlab::Metrics).to receive(:counter)
- .with(:gitlab_memwd_violations_handled_total, anything, anything)
- .and_return(violations_handled_counter)
-
- allow(violations_counter).to receive(:increment)
- allow(violations_handled_counter).to receive(:increment)
- end
-
describe '#initialize' do
it 'initialize new configuration' do
expect(described_class::Configuration).to receive(:new)
@@ -59,33 +45,25 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures do
describe '#call' do
before do
- stub_prometheus_metrics
- allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(
- total: 1024
- )
- allow(::Prometheus::PidProvider).to receive(:worker_id).and_return('worker_1')
-
watchdog.configure do |config|
config.handler = handler
- config.logger = logger
+ config.event_reporter = reporter
config.sleep_time_seconds = sleep_time_seconds
- config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes
end
allow(handler).to receive(:call).and_return(true)
- allow(logger).to receive(:info)
- allow(logger).to receive(:warn)
+ allow(reporter).to receive(:started)
+ allow(reporter).to receive(:stopped)
+ allow(reporter).to receive(:threshold_violated)
+ allow(reporter).to receive(:strikes_exceeded)
end
- it 'logs start message once' do
- expect(logger).to receive(:info).once
+ it 'reports started event once' do
+ expect(reporter).to receive(:started).once
.with(
- pid: Process.pid,
- worker_id: 'worker_1',
memwd_handler_class: handler.class.name,
- memwd_sleep_time_s: sleep_time_seconds,
- memwd_rss_bytes: 1024,
- message: 'started')
+ memwd_sleep_time_s: sleep_time_seconds
+ )
watchdog.call
end
@@ -96,55 +74,50 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures do
watchdog.call
end
- context 'when gitlab_memory_watchdog ops toggle is off' do
- before do
- stub_feature_flags(gitlab_memory_watchdog: false)
- end
-
- it 'does not trigger any monitor' do
- expect(configuration).not_to receive(:monitors)
- end
- end
-
- context 'when process does not exceed threshold' do
- it 'does not increment violations counters' do
- expect(violations_counter).not_to receive(:increment)
- expect(violations_handled_counter).not_to receive(:increment)
-
- watchdog.call
- end
-
- it 'does not log violation' do
- expect(logger).not_to receive(:warn)
-
- watchdog.call
- end
-
- it 'does not execute handler' do
- expect(handler).not_to receive(:call)
+ context 'when no monitors are configured' do
+ it 'reports stopped event once with correct reason' do
+ expect(reporter).to receive(:stopped).once
+ .with(
+ memwd_handler_class: handler.class.name,
+ memwd_sleep_time_s: sleep_time_seconds,
+ memwd_reason: 'monitors are not configured'
+ )
watchdog.call
end
end
- context 'when process exceeds threshold' do
- let(:threshold_violated) { true }
+ context 'when monitors are configured' do
+ before do
+ watchdog.configure do |config|
+ config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes
+ end
+ end
- it 'increments violations counter' do
- expect(violations_counter).to receive(:increment).with(reason: name)
+ it 'reports stopped event once' do
+ expect(reporter).to receive(:stopped).once
+ .with(
+ memwd_handler_class: handler.class.name,
+ memwd_sleep_time_s: sleep_time_seconds
+ )
watchdog.call
end
- context 'when process does not exceed the allowed number of strikes' do
- it 'does not increment handled violations counter' do
- expect(violations_handled_counter).not_to receive(:increment)
+ context 'when gitlab_memory_watchdog ops toggle is off' do
+ before do
+ stub_feature_flags(gitlab_memory_watchdog: false)
+ end
- watchdog.call
+ it 'does not trigger any monitor' do
+ expect(configuration).not_to receive(:monitors)
end
+ end
- it 'does not log violation' do
- expect(logger).not_to receive(:warn)
+ context 'when process does not exceed threshold' do
+ it 'does not report violations event' do
+ expect(reporter).not_to receive(:threshold_violated)
+ expect(reporter).not_to receive(:strikes_exceeded)
watchdog.call
end
@@ -156,81 +129,94 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures do
end
end
- context 'when monitor exceeds the allowed number of strikes' do
- let(:max_strikes) { 0 }
+ context 'when process exceeds threshold' do
+ let(:threshold_violated) { true }
- it 'increments handled violations counter' do
- expect(violations_handled_counter).to receive(:increment).with(reason: name)
+ it 'reports threshold violated event' do
+ expect(reporter).to receive(:threshold_violated).with(name)
watchdog.call
end
- it 'logs violation' do
- expect(logger).to receive(:warn)
- .with(
- pid: Process.pid,
- worker_id: 'worker_1',
- memwd_handler_class: handler.class.name,
- memwd_sleep_time_s: sleep_time_seconds,
- memwd_rss_bytes: 1024,
- memwd_cur_strikes: 1,
- memwd_max_strikes: max_strikes,
- message: 'dummy_text')
+ context 'when process does not exceed the allowed number of strikes' do
+ it 'does not report strikes exceeded event' do
+ expect(reporter).not_to receive(:strikes_exceeded)
- watchdog.call
- end
+ watchdog.call
+ end
- it 'executes handler' do
- expect(handler).to receive(:call)
+ it 'does not execute handler' do
+ expect(handler).not_to receive(:call)
- watchdog.call
+ watchdog.call
+ end
end
- context 'when enforce_memory_watchdog ops toggle is off' do
- before do
- stub_feature_flags(enforce_memory_watchdog: false)
+ context 'when monitor exceeds the allowed number of strikes' do
+ let(:max_strikes) { 0 }
+
+ it 'reports strikes exceeded event' do
+ expect(reporter).to receive(:strikes_exceeded)
+ .with(
+ name,
+ memwd_handler_class: handler.class.name,
+ memwd_sleep_time_s: sleep_time_seconds,
+ memwd_cur_strikes: 1,
+ memwd_max_strikes: max_strikes,
+ message: "dummy_text"
+ )
+
+ watchdog.call
end
- it 'always uses the NullHandler' do
- expect(handler).not_to receive(:call)
- expect(described_class::NullHandler.instance).to receive(:call).and_return(true)
+ it 'executes handler and stops the watchdog' do
+ expect(handler).to receive(:call).and_return(true)
+ expect(reporter).to receive(:stopped).once
+ .with(
+ memwd_handler_class: handler.class.name,
+ memwd_sleep_time_s: sleep_time_seconds,
+ memwd_reason: 'successfully handled'
+ )
watchdog.call
end
- end
- context 'when multiple monitors exceeds allowed number of strikes' do
- before do
- watchdog.configure do |config|
- config.handler = handler
- config.logger = logger
- config.sleep_time_seconds = sleep_time_seconds
- config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes
- config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes
+ it 'schedules a heap dump' do
+ expect(Gitlab::Memory::Reports::HeapDump).to receive(:enqueue!)
+
+ watchdog.call
+ end
+
+ context 'when enforce_memory_watchdog ops toggle is off' do
+ before do
+ stub_feature_flags(enforce_memory_watchdog: false)
+ end
+
+ it 'always uses the NullHandler' do
+ expect(handler).not_to receive(:call)
+ expect(described_class::NullHandler.instance).to receive(:call).and_return(true)
+
+ watchdog.call
end
end
- it 'only calls the handler once' do
- expect(handler).to receive(:call).once.and_return(true)
+ context 'when multiple monitors exceeds allowed number of strikes' do
+ before do
+ watchdog.configure do |config|
+ config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes
+ config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes
+ end
+ end
+
+ it 'only calls the handler once' do
+ expect(handler).to receive(:call).once.and_return(true)
- watchdog.call
+ watchdog.call
+ end
end
end
end
end
-
- it 'logs stop message once' do
- expect(logger).to receive(:info).once
- .with(
- pid: Process.pid,
- worker_id: 'worker_1',
- memwd_handler_class: handler.class.name,
- memwd_sleep_time_s: sleep_time_seconds,
- memwd_rss_bytes: 1024,
- message: 'stopped')
-
- watchdog.call
- end
end
describe '#configure' do
@@ -255,6 +241,10 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures do
subject(:handler) { described_class::TermProcessHandler.new(42) }
describe '#call' do
+ before do
+ allow(Process).to receive(:kill)
+ end
+
it 'sends SIGTERM to the current process' do
expect(Process).to receive(:kill).with(:TERM, 42)
@@ -274,11 +264,12 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures do
before do
stub_const('::Puma::Cluster::WorkerHandle', puma_worker_handle_class)
+ allow(puma_worker_handle_class).to receive(:new).and_return(puma_worker_handle)
+ allow(puma_worker_handle).to receive(:term)
end
describe '#call' do
it 'invokes orderly termination via Puma API' do
- expect(puma_worker_handle_class).to receive(:new).and_return(puma_worker_handle)
expect(puma_worker_handle).to receive(:term)
expect(handler.call).to be(true)
diff --git a/spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb b/spec/lib/gitlab/merge_requests/message_generator_spec.rb
index ad528dca81a..59aaffc4377 100644
--- a/spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb
+++ b/spec/lib/gitlab/merge_requests/message_generator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
+RSpec.describe Gitlab::MergeRequests::MessageGenerator, feature_category: :code_review do
let(:merge_commit_template) { nil }
let(:squash_commit_template) { nil }
let(:project) do
@@ -59,7 +59,14 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
context 'when project has commit template with only the title' do
let(:merge_request) do
- double(:merge_request, title: 'Fixes', target_project: project, to_reference: '!123', metrics: nil, merge_user: nil)
+ double(
+ :merge_request,
+ title: 'Fixes',
+ target_project: project,
+ to_reference: '!123',
+ metrics: nil,
+ merge_user: nil
+ )
end
let(message_template_name) { '%{title}' }
@@ -214,7 +221,7 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
context 'when project has template with CRLF newlines' do
let(message_template_name) do
- "Merge branch '%{source_branch}' into '%{target_branch}'\r\n\r\n%{title}\r\n\r\n%{description}\r\n\r\nSee merge request %{reference}"
+ "Merge branch '%{source_branch}' into '%{target_branch}'\r\n\r\n%{title}\r\n\r\n%{description}\r\n\r\nSee merge request %{reference}" # rubocop: disable Layout/LineLength
end
it 'converts it to LF newlines' do
@@ -289,6 +296,93 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
end
end
+ context 'when project has merge commit template with reviewers' do
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(message_template_name) { <<~MSG.rstrip }
+ Merge branch '%{source_branch}' into '%{target_branch}'
+
+ %{reviewed_by}
+ MSG
+
+ context 'and mr has no reviewers' do
+ before do
+ merge_request.reviews = []
+ end
+
+ it 'removes variable and blank line' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+ MSG
+ end
+
+ context 'when there is blank line after reviewed_by' do
+ let(message_template_name) { <<~MSG.rstrip }
+ Merge branch '%{source_branch}' into '%{target_branch}'
+
+ %{reviewed_by}
+
+ Type: merge
+ MSG
+
+ it 'removes blank line before it' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Type: merge
+ MSG
+ end
+ end
+
+ context 'when there is no blank line after reviewed_by' do
+ let(message_template_name) { <<~MSG.rstrip }
+ Merge branch '%{source_branch}' into '%{target_branch}'
+
+ %{reviewed_by}
+ Type: merge
+ MSG
+
+ it 'does not remove blank line before it' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Type: merge
+ MSG
+ end
+ end
+ end
+
+ context 'and mr has one reviewer' do
+ before do
+ merge_request.reviews.create!(project: merge_request.project, author: user1)
+ end
+
+ it 'returns user name and email' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Reviewed-by: #{user1.name} <#{user1.email}>
+ MSG
+ end
+ end
+
+ context 'and mr has multiple reviewers' do
+ before do
+ merge_request.reviews.create!(project: merge_request.project, author: user1)
+ merge_request.reviews.create!(project: merge_request.project, author: user2)
+ end
+
+ it 'returns users names and emails' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Reviewed-by: #{user1.name} <#{user1.email}>
+ Reviewed-by: #{user2.name} <#{user2.email}>
+ MSG
+ end
+ end
+ end
+
context 'when project has merge commit template with approvers' do
let(:user1) { create(:user) }
let(:user2) { create(:user) }
@@ -547,6 +641,7 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
first_commit:%{first_commit}
first_multiline_commit:%{first_multiline_commit}
url:%{url}
+ reviewed_by:%{reviewed_by}
approved_by:%{approved_by}
merged_by:%{merged_by}
co_authored_by:%{co_authored_by}
@@ -568,6 +663,7 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
url:#{Gitlab::UrlBuilder.build(merge_request)}
+ reviewed_by:
approved_by:
merged_by:#{current_user.name} <#{current_user.commit_email_or_default}>
co_authored_by:Co-authored-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
@@ -628,8 +724,8 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
end
end
- describe '#merge_message' do
- let(:result_message) { subject.merge_message }
+ describe '#merge_commit_message' do
+ let(:result_message) { subject.merge_commit_message }
it_behaves_like 'commit message with template', :merge_commit_template
@@ -660,8 +756,8 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
end
end
- describe '#squash_message' do
- let(:result_message) { subject.squash_message }
+ describe '#squash_commit_message' do
+ let(:result_message) { subject.squash_commit_message }
it_behaves_like 'commit message with template', :squash_commit_template
@@ -691,4 +787,95 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
end
end
end
+
+ describe '#new_mr_description' do
+ let(:merge_request) do
+ build(
+ :merge_request,
+ source_project: project,
+ target_project: project,
+ target_branch: 'master',
+ source_branch: source_branch,
+ author: author,
+ description: merge_request_description,
+ title: merge_request_title
+ )
+ end
+
+ let(:result_message) { subject.new_mr_description }
+
+ before do
+ compare = CompareService.new(
+ project,
+ merge_request.source_branch
+ ).execute(
+ project,
+ merge_request.target_branch
+ )
+
+ merge_request.compare_commits = compare.commits
+ merge_request.compare = compare
+ end
+
+ context 'when project has template with all variables' do
+ let(:merge_request_description) { <<~MSG.rstrip }
+ source_branch:%{source_branch}
+ target_branch:%{target_branch}
+ title:%{title}
+ issues:%{issues}
+ description:%{description}
+ first_commit:%{first_commit}
+ first_multiline_commit:%{first_multiline_commit}
+ url:%{url}
+ approved_by:%{approved_by}
+ merged_by:%{merged_by}
+ co_authored_by:%{co_authored_by}
+ all_commits:%{all_commits}
+ MSG
+
+ it 'renders only variables specific to a new non-persisted merge request' do
+ expect(result_message).to eq <<~MSG.rstrip
+ source_branch:feature
+ target_branch:master
+ title:
+ issues:
+ description:
+ first_commit:Feature added
+
+ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+ first_multiline_commit:Feature added
+
+ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+ url:
+ approved_by:
+ merged_by:
+ co_authored_by:Co-authored-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+ all_commits:* Feature added
+
+ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+ MSG
+ end
+
+ context 'when no first commit exists' do
+ let(:source_branch) { 'master' }
+
+ it 'does not populate any commit-related variables' do
+ expect(result_message).to eq <<~MSG.rstrip
+ source_branch:master
+ target_branch:master
+ title:
+ issues:
+ description:
+ first_commit:
+ first_multiline_commit:Bugfix
+ url:
+ approved_by:
+ merged_by:
+ co_authored_by:
+ all_commits:
+ MSG
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb
index aaa9daf8fee..fb55b736354 100644
--- a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb
@@ -143,56 +143,4 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do
end
end
end
-
- describe '#errors' do
- context 'valid dashboard schema' do
- it 'returns no errors' do
- expect(described_class.errors(valid_dashboard)).to eq []
- end
-
- context 'with duplicate metric_ids' do
- it 'returns errors' do
- expect(described_class.errors(duplicate_id_dashboard)).to eq [Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds.new]
- end
- end
-
- context 'with dashboard_path and project' do
- subject { described_class.errors(valid_dashboard, dashboard_path: 'test/path.yml', project: project) }
-
- context 'with no conflicting metric identifiers in db' do
- it { is_expected.to eq [] }
- end
-
- context 'with metric identifier present in current dashboard' do
- before do
- create(:prometheus_metric,
- identifier: 'metric_a1',
- dashboard_path: 'test/path.yml',
- project: project
- )
- end
-
- it { is_expected.to eq [] }
- end
-
- context 'with metric identifier present in another dashboard' do
- before do
- create(:prometheus_metric,
- identifier: 'metric_a1',
- dashboard_path: 'some/other/dashboard/path.yml',
- project: project
- )
- end
-
- it { is_expected.to eq [Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds.new] }
- end
- end
- end
-
- context 'invalid dashboard schema' do
- it 'returns collection of validation errors' do
- expect(described_class.errors(invalid_dashboard)).to all be_kind_of(Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError)
- end
- end
- end
end
diff --git a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
index fa50adb4e4f..6673cc50d67 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 do
+RSpec.describe Gitlab::Metrics::Exporter::BaseExporter, feature_category: :application_performance 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/global_search_slis_spec.rb b/spec/lib/gitlab/metrics/global_search_slis_spec.rb
index c10d83664ea..1aa2c4398a7 100644
--- a/spec/lib/gitlab/metrics/global_search_slis_spec.rb
+++ b/spec/lib/gitlab/metrics/global_search_slis_spec.rb
@@ -5,12 +5,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Metrics::GlobalSearchSlis do
using RSpec::Parameterized::TableSyntax
- let(:error_rate_feature_flag_enabled) { true }
-
- before do
- stub_feature_flags(global_search_error_rate_sli: error_rate_feature_flag_enabled)
- end
-
describe '#initialize_slis!' do
it 'initializes Apdex SLIs for global_search' do
expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(
@@ -21,27 +15,13 @@ RSpec.describe Gitlab::Metrics::GlobalSearchSlis do
described_class.initialize_slis!
end
- context 'when global_search_error_rate_sli feature flag is enabled' do
- let(:error_rate_feature_flag_enabled) { true }
-
- it 'initializes ErrorRate SLIs for global_search' do
- expect(Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli).with(
- :global_search,
- a_kind_of(Array)
- )
-
- described_class.initialize_slis!
- end
- end
-
- context 'when global_search_error_rate_sli feature flag is disabled' do
- let(:error_rate_feature_flag_enabled) { false }
-
- it 'does not initialize the ErrorRate SLIs for global_search' do
- expect(Gitlab::Metrics::Sli::ErrorRate).not_to receive(:initialize_sli)
+ it 'initializes ErrorRate SLIs for global_search' do
+ expect(Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli).with(
+ :global_search,
+ a_kind_of(Array)
+ )
- described_class.initialize_slis!
- end
+ described_class.initialize_slis!
end
end
@@ -105,34 +85,15 @@ RSpec.describe Gitlab::Metrics::GlobalSearchSlis do
end
describe '#record_error_rate' do
- context 'when global_search_error_rate_sli feature flag is enabled' do
- let(:error_rate_feature_flag_enabled) { true }
-
- it 'calls increment on the error rate SLI' do
- expect(Gitlab::Metrics::Sli::ErrorRate[:global_search]).to receive(:increment)
-
- described_class.record_error_rate(
- error: true,
- search_type: 'basic',
- search_level: 'global',
- search_scope: 'issues'
- )
- end
- end
-
- context 'when global_search_error_rate_sli feature flag is disabled' do
- let(:error_rate_feature_flag_enabled) { false }
-
- it 'does not call increment on the error rate SLI' do
- expect(Gitlab::Metrics::Sli::ErrorRate[:global_search]).not_to receive(:increment)
-
- described_class.record_error_rate(
- error: true,
- search_type: 'basic',
- search_level: 'global',
- search_scope: 'issues'
- )
- end
+ it 'calls increment on the error rate SLI' do
+ expect(Gitlab::Metrics::Sli::ErrorRate[:global_search]).to receive(:increment)
+
+ described_class.record_error_rate(
+ error: true,
+ search_type: 'basic',
+ search_level: 'global',
+ search_scope: 'issues'
+ )
end
end
end
diff --git a/spec/lib/gitlab/metrics/rails_slis_spec.rb b/spec/lib/gitlab/metrics/rails_slis_spec.rb
index b30eb57101f..9da102fb8b8 100644
--- a/spec/lib/gitlab/metrics/rails_slis_spec.rb
+++ b/spec/lib/gitlab/metrics/rails_slis_spec.rb
@@ -14,8 +14,8 @@ RSpec.describe Gitlab::Metrics::RailsSlis do
end
describe '.initialize_request_slis!' do
- it "initializes the SLI for all possible endpoints if they weren't", :aggregate_failures do
- possible_labels = [
+ let(:possible_labels) do
+ [
{
endpoint_id: "GET /api/:version/version",
feature_category: :not_owned,
@@ -27,17 +27,32 @@ RSpec.describe Gitlab::Metrics::RailsSlis do
request_urgency: :default
}
]
+ end
- possible_graphql_labels = ['graphql:foo', 'graphql:bar', 'graphql:unknown'].map do |endpoint_id|
+ let(:possible_graphql_labels) do
+ ['graphql:foo', 'graphql:bar', 'graphql:unknown'].map do |endpoint_id|
{
endpoint_id: endpoint_id,
feature_category: nil,
query_urgency: ::Gitlab::EndpointAttributes::DEFAULT_URGENCY.name
}
end
+ end
+
+ it "initializes the SLI for all possible endpoints if they weren't", :aggregate_failures do
+ expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(:rails_request, array_including(*possible_labels)).and_call_original
+ expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(:graphql_query, array_including(*possible_graphql_labels)).and_call_original
+ expect(Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli).with(:rails_request, array_including(*possible_labels)).and_call_original
+
+ described_class.initialize_request_slis!
+ end
+
+ it "initializes the SLI for all possible endpoints if they weren't given error rate feature flag is disabled", :aggregate_failures do
+ stub_feature_flags(gitlab_metrics_error_rate_sli: false)
expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(:rails_request, array_including(*possible_labels)).and_call_original
expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(:graphql_query, array_including(*possible_graphql_labels)).and_call_original
+ expect(Gitlab::Metrics::Sli::ErrorRate).not_to receive(:initialize_sli)
described_class.initialize_request_slis!
end
@@ -51,6 +66,14 @@ RSpec.describe Gitlab::Metrics::RailsSlis do
end
end
+ describe '.request_error' do
+ it 'returns the initialized request error rate SLI object' do
+ described_class.initialize_request_slis!
+
+ expect(described_class.request_error_rate).to be_initialized
+ end
+ end
+
describe '.graphql_query_apdex' do
it 'returns the initialized request apdex SLI object' do
described_class.initialize_request_slis!
diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
index ed78548ef62..61c690b85e9 100644
--- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
+RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures, feature_category: :error_budgets do
let(:app) { double('app') }
subject { described_class.new(app) }
@@ -38,6 +38,29 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ method: 'get' }, a_positive_execution_time)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment)
.with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, success: true)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment)
+ .with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, error: false)
+
+ subject.call(env)
+ end
+
+ it 'guarantees SLI metrics are incremented with all the required labels' do
+ described_class.initialize_metrics
+
+ expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).and_call_original
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).and_call_original
+
+ subject.call(env)
+ end
+
+ it 'does not track error rate when feature flag is disabled' do
+ stub_feature_flags(gitlab_metrics_error_rate_sli: false)
+
+ expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '200', feature_category: 'unknown')
+ expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ method: 'get' }, a_positive_execution_time)
+ expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment)
+ .with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, success: true)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).not_to receive(:increment)
subject.call(env)
end
@@ -84,10 +107,23 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
context '@app.call returns an error code' do
let(:status) { '500' }
- it 'tracks count but not duration or apdex' do
+ it 'tracks count and error rate but not duration and apdex' do
+ expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '500', feature_category: 'unknown')
+ expect(described_class).not_to receive(:http_request_duration_seconds)
+ expect(Gitlab::Metrics::RailsSlis).not_to receive(:request_apdex)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment)
+ .with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, error: true)
+
+ subject.call(env)
+ end
+
+ it 'does not track error rate when feature flag is disabled' do
+ stub_feature_flags(gitlab_metrics_error_rate_sli: false)
+
expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '500', feature_category: 'unknown')
expect(described_class).not_to receive(:http_request_duration_seconds)
expect(Gitlab::Metrics::RailsSlis).not_to receive(:request_apdex)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).not_to receive(:increment)
subject.call(env)
end
@@ -108,6 +144,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: 'undefined', feature_category: 'unknown')
expect(described_class.http_request_duration_seconds).not_to receive(:observe)
expect(Gitlab::Metrics::RailsSlis).not_to receive(:request_apdex)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).not_to receive(:increment)
expect { subject.call(env) }.to raise_error(StandardError)
end
@@ -124,6 +161,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(described_class).not_to receive(:http_health_requests_total)
expect(Gitlab::Metrics::RailsSlis.request_apdex)
.to receive(:increment).with(labels: { feature_category: 'team_planning', endpoint_id: 'IssuesController#show', request_urgency: :default }, success: true)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment)
+ .with(labels: { feature_category: 'team_planning', endpoint_id: 'IssuesController#show', request_urgency: :default }, error: false)
subject.call(env)
end
@@ -134,6 +173,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(described_class).to receive_message_chain(:http_health_requests_total, :increment).with(method: 'get', status: '200')
expect(described_class).not_to receive(:http_requests_total)
expect(Gitlab::Metrics::RailsSlis).not_to receive(:request_apdex)
+ expect(Gitlab::Metrics::RailsSlis).not_to receive(:request_error_rate)
subject.call(env)
end
@@ -147,8 +187,9 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
it 'adds the feature category to the labels for http_requests_total' do
expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: 'undefined', feature_category: 'team_planning')
- expect(Gitlab::Metrics::RailsSlis).not_to receive(:request_apdex)
+ expect(Gitlab::Metrics::RailsSlis).not_to receive(:request_apdex)
+ expect(Gitlab::Metrics::RailsSlis).not_to receive(:request_error_rate)
expect { subject.call(env) }.to raise_error(StandardError)
end
end
@@ -159,6 +200,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(described_class).not_to receive(:http_health_requests_total)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment)
.with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, success: true)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment)
+ .with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, error: false)
subject.call(env)
end
@@ -214,6 +257,15 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
},
success: success
)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
+ labels: {
+ feature_category: 'hello_world',
+ endpoint_id: 'GET /projects/:id/archive',
+ request_urgency: request_urgency_name
+ },
+ error: false
+ )
+
subject.call(env)
end
end
@@ -247,6 +299,15 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
},
success: success
)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
+ labels: {
+ feature_category: 'hello_world',
+ endpoint_id: 'AnonymousController#index',
+ request_urgency: request_urgency_name
+ },
+ error: false
+ )
+
subject.call(env)
end
end
@@ -273,6 +334,14 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
},
success: true
)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :default
+ },
+ error: false
+ )
subject.call(env)
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 101)
@@ -284,6 +353,14 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
},
success: false
)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :default
+ },
+ error: false
+ )
subject.call(env)
end
end
@@ -307,6 +384,14 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
},
success: true
)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :default
+ },
+ error: false
+ )
subject.call(env)
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 101)
@@ -318,6 +403,14 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
},
success: false
)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :default
+ },
+ error: false
+ )
subject.call(env)
end
end
@@ -337,6 +430,14 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
},
success: true
)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :default
+ },
+ error: false
+ )
subject.call(env)
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 101)
@@ -348,6 +449,14 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
},
success: false
)
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :default
+ },
+ error: false
+ )
subject.call(env)
end
end
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index 005c1ae2d0a..4569f3134ae 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
let(:env) { {} }
let(:subscriber) { described_class.new }
- let(:connection) { ActiveRecord::Base.retrieve_connection }
+
+ let(:connection) { Gitlab::Database.database_base_models[:main].retrieve_connection }
let(:db_config_name) { ::Gitlab::Database.db_config_name(connection) }
describe '.load_balancing_metric_counter_keys' do
@@ -155,7 +156,9 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
end
it 'captures the metrics for web only' do
- expect(web_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23, { db_config_name: db_config_name })
+ expect(web_transaction).to receive(:observe).with(
+ :gitlab_database_transaction_seconds, 0.23, { db_config_name: db_config_name }
+ )
expect(background_transaction).not_to receive(:observe)
expect(background_transaction).not_to receive(:increment)
@@ -175,7 +178,9 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
end
it 'captures the metrics for web only' do
- expect(web_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23, { db_config_name: db_config_name })
+ expect(web_transaction).to receive(:observe).with(
+ :gitlab_database_transaction_seconds, 0.23, { db_config_name: db_config_name }
+ )
expect(background_transaction).not_to receive(:observe)
expect(background_transaction).not_to receive(:increment)
@@ -195,7 +200,9 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
end
it 'captures the metrics for web only' do
- expect(background_transaction).to receive(:observe).with(:gitlab_database_transaction_seconds, 0.23, { db_config_name: db_config_name })
+ expect(background_transaction).to receive(:observe).with(
+ :gitlab_database_transaction_seconds, 0.23, { db_config_name: db_config_name }
+ )
expect(web_transaction).not_to receive(:observe)
expect(web_transaction).not_to receive(:increment)
diff --git a/spec/lib/gitlab/metrics/subscribers/ldap_spec.rb b/spec/lib/gitlab/metrics/subscribers/ldap_spec.rb
new file mode 100644
index 00000000000..b81000be62a
--- /dev/null
+++ b/spec/lib/gitlab/metrics/subscribers/ldap_spec.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Gitlab::Metrics::Subscribers::Ldap, :request_store, feature_category: :logging do
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new({}) }
+ let(:subscriber) { described_class.new }
+
+ let(:attributes) do
+ [
+ :altServer, :namingContexts, :supportedCapabilities, :supportedControl,
+ :supportedExtension, :supportedFeatures, :supportedLdapVersion, :supportedSASLMechanisms
+ ]
+ end
+
+ let(:event_1) do
+ instance_double(
+ ActiveSupport::Notifications::Event,
+ name: "open.net_ldap",
+ payload: {
+ ignore_server_caps: true,
+ base: "",
+ scope: 0,
+ attributes: attributes,
+ result: nil
+ },
+ time: Time.current,
+ duration: 0.321
+ )
+ end
+
+ let(:event_2) do
+ instance_double(
+ ActiveSupport::Notifications::Event,
+ name: "search.net_ldap",
+ payload: {
+ ignore_server_caps: true,
+ base: "",
+ scope: 0,
+ attributes: attributes,
+ result: nil
+ },
+ time: Time.current,
+ duration: 0.12
+ )
+ end
+
+ let(:event_3) do
+ instance_double(
+ ActiveSupport::Notifications::Event,
+ name: "search.net_ldap",
+ payload: {
+ ignore_server_caps: true,
+ base: "",
+ scope: 0,
+ attributes: attributes,
+ result: nil
+ },
+ time: Time.current,
+ duration: 5.3
+ )
+ end
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ describe ".payload" do
+ context "when SafeRequestStore is empty" do
+ it "returns an empty array" do
+ expect(described_class.payload).to eql(net_ldap_count: 0, net_ldap_duration_s: 0.0)
+ end
+ end
+
+ context "when LDAP recorded some values" do
+ before do
+ Gitlab::SafeRequestStore[:net_ldap_count] = 7
+ Gitlab::SafeRequestStore[:net_ldap_duration_s] = 1.2
+ end
+
+ it "returns the populated payload" do
+ expect(described_class.payload).to eql(net_ldap_count: 7, net_ldap_duration_s: 1.2)
+ end
+ end
+ end
+
+ describe "#observe_event" do
+ before do
+ allow(subscriber).to receive(:current_transaction).and_return(transaction)
+ end
+
+ it "tracks LDAP request count" do
+ expect(transaction).to receive(:increment)
+ .with(:gitlab_net_ldap_total, 1, { name: "open" })
+ expect(transaction).to receive(:increment)
+ .with(:gitlab_net_ldap_total, 1, { name: "search" })
+
+ subscriber.observe_event(event_1)
+ subscriber.observe_event(event_2)
+ end
+
+ it "tracks LDAP request duration" do
+ expect(transaction).to receive(:observe)
+ .with(:gitlab_net_ldap_duration_seconds, 0.321, { name: "open" })
+ expect(transaction).to receive(:observe)
+ .with(:gitlab_net_ldap_duration_seconds, 0.12, { name: "search" })
+ expect(transaction).to receive(:observe)
+ .with(:gitlab_net_ldap_duration_seconds, 5.3, { name: "search" })
+
+ subscriber.observe_event(event_1)
+ subscriber.observe_event(event_2)
+ subscriber.observe_event(event_3)
+ end
+
+ it "stores per-request counters" do
+ subscriber.observe_event(event_1)
+ subscriber.observe_event(event_2)
+ subscriber.observe_event(event_3)
+
+ expect(Gitlab::SafeRequestStore[:net_ldap_count]).to eq(3)
+ expect(Gitlab::SafeRequestStore[:net_ldap_duration_s]).to eq(5.741) # 0.321 + 0.12 + 5.3
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
index 9aba6ac293c..59bfe2042fa 100644
--- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do
let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
let(:subscriber) { described_class.new }
- let(:event) { double(:event, duration: 15.2) }
+ let(:event) { double(:event, duration: 15.2, payload: { key: %w[a b c] }) }
describe '#cache_read' do
it 'increments the cache_read duration' do
@@ -64,6 +64,40 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do
end
end
+ describe '#cache_read_multi' do
+ subject { subscriber.cache_read_multi(event) }
+
+ context 'with a transaction' do
+ before do
+ allow(subscriber).to receive(:current_transaction)
+ .and_return(transaction)
+ end
+
+ it 'observes multi-key count' do
+ expect(transaction).to receive(:observe)
+ .with(:gitlab_cache_read_multikey_count, event.payload[:key].size)
+
+ subject
+ end
+ end
+
+ context 'with no transaction' do
+ it 'does not observes multi-key count' do
+ expect(transaction).not_to receive(:observe)
+ .with(:gitlab_cache_read_multikey_count, event.payload[:key].size)
+
+ subject
+ end
+ end
+
+ it 'observes read_multi duration' do
+ expect(subscriber).to receive(:observe)
+ .with(:read_multi, event.duration)
+
+ subject
+ end
+ end
+
describe '#cache_write' do
it 'observes write duration' do
expect(subscriber).to receive(:observe)
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 366843a4c03..dbd6c07ef75 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -101,14 +101,32 @@ RSpec.describe Gitlab::Metrics do
401 | true
nil | false
500 | false
- 503 | false
- '100' | false
- '201' | true
+ 503 | false
'nothing' | false
end
with_them do
specify { expect(described_class.record_duration_for_status?(status)).to be(should_record) }
+ specify { expect(described_class.record_duration_for_status?(status.to_s)).to be(should_record) }
+ end
+ end
+
+ describe '.server_error?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :should_record) do
+ 100 | false
+ 200 | false
+ 401 | false
+ 500 | true
+ 503 | true
+ nil | false
+ 'nothing' | false
+ end
+
+ with_them do
+ specify { expect(described_class.server_error?(status)).to be(should_record) }
+ specify { expect(described_class.server_error?(status.to_s)).to be(should_record) }
end
end
diff --git a/spec/lib/gitlab/middleware/compressed_json_spec.rb b/spec/lib/gitlab/middleware/compressed_json_spec.rb
index 6d49ab58d5d..1444e6a9881 100644
--- a/spec/lib/gitlab/middleware/compressed_json_spec.rb
+++ b/spec/lib/gitlab/middleware/compressed_json_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe Gitlab::Middleware::CompressedJson do
let(:app) { double(:app) }
let(:middleware) { described_class.new(app) }
let(:content_type) { 'application/json' }
+ let(:relative_url_root) { '/gitlab' }
let(:env) do
{
'HTTP_CONTENT_ENCODING' => 'gzip',
@@ -31,6 +32,43 @@ RSpec.describe Gitlab::Middleware::CompressedJson do
end
end
+ shared_examples 'passes input' do
+ it 'keeps the original input' do
+ expect(app).to receive(:call)
+
+ middleware.call(env)
+
+ expect(env['rack.input'].read).to eq(input)
+ expect(env['HTTP_CONTENT_ENCODING']).to eq('gzip')
+ end
+ end
+
+ shared_context 'with relative url' do
+ before do
+ stub_config_setting(relative_url_root: relative_url_root)
+ end
+ end
+
+ shared_examples 'handles non integer project ID' do
+ context 'with a URL-encoded project ID' do
+ let_it_be(:project_id) { 'gitlab-org%2fgitlab' }
+
+ it_behaves_like 'decompress middleware'
+ end
+
+ context 'with a non URL-encoded project ID' do
+ let_it_be(:project_id) { '1/repository/files/api/v4' }
+
+ it_behaves_like 'passes input'
+ end
+
+ context 'with a blank project ID' do
+ let_it_be(:project_id) { '' }
+
+ it_behaves_like 'passes input'
+ end
+ end
+
describe '#call' do
context 'with collector route' do
let(:path) { '/api/v4/error_tracking/collector/1/store' }
@@ -42,31 +80,80 @@ RSpec.describe Gitlab::Middleware::CompressedJson do
it_behaves_like 'decompress middleware'
end
+
+ include_context 'with relative url' do
+ let(:path) { "#{relative_url_root}/api/v4/error_tracking/collector/1/store" }
+
+ it_behaves_like 'decompress middleware'
+ end
end
- context 'with collector route under relative url' do
- let(:path) { '/gitlab/api/v4/error_tracking/collector/1/store' }
+ context 'with packages route' do
+ context 'with instance level endpoint' do
+ context 'with npm advisory bulk url' do
+ let(:path) { '/api/v4/packages/npm/-/npm/v1/security/advisories/bulk' }
+
+ it_behaves_like 'decompress middleware'
+
+ include_context 'with relative url' do
+ let(:path) { "#{relative_url_root}/api/v4/packages/npm/-/npm/v1/security/advisories/bulk" }
+
+ it_behaves_like 'decompress middleware'
+ end
+ end
+
+ context 'with npm quick audit url' do
+ let(:path) { '/api/v4/packages/npm/-/npm/v1/security/audits/quick' }
- before do
- stub_config_setting(relative_url_root: '/gitlab')
+ it_behaves_like 'decompress middleware'
+
+ include_context 'with relative url' do
+ let(:path) { "#{relative_url_root}/api/v4/packages/npm/-/npm/v1/security/audits/quick" }
+
+ it_behaves_like 'decompress middleware'
+ end
+ end
end
- it_behaves_like 'decompress middleware'
- end
+ context 'with project level endpoint' do
+ let_it_be(:project_id) { 1 }
- context 'with some other route' do
- let(:path) { '/api/projects/123' }
+ context 'with npm advisory bulk url' do
+ let(:path) { "/api/v4/projects/#{project_id}/packages/npm/-/npm/v1/security/advisories/bulk" }
- it 'keeps the original input' do
- expect(app).to receive(:call)
+ it_behaves_like 'decompress middleware'
- middleware.call(env)
+ include_context 'with relative url' do
+ let(:path) { "#{relative_url_root}/api/v4/projects/#{project_id}/packages/npm/-/npm/v1/security/advisories/bulk" } # rubocop disable Layout/LineLength
- expect(env['rack.input'].read).to eq(input)
- expect(env['HTTP_CONTENT_ENCODING']).to eq('gzip')
+ it_behaves_like 'decompress middleware'
+ end
+
+ it_behaves_like 'handles non integer project ID'
+ end
+
+ context 'with npm quick audit url' do
+ let(:path) { "/api/v4/projects/#{project_id}/packages/npm/-/npm/v1/security/audits/quick" }
+
+ it_behaves_like 'decompress middleware'
+
+ include_context 'with relative url' do
+ let(:path) { "#{relative_url_root}/api/v4/projects/#{project_id}/packages/npm/-/npm/v1/security/audits/quick" } # rubocop disable Layout/LineLength
+
+ it_behaves_like 'decompress middleware'
+ end
+
+ it_behaves_like 'handles non integer project ID'
+ end
end
end
+ context 'with some other route' do
+ let(:path) { '/api/projects/123' }
+
+ it_behaves_like 'passes input'
+ end
+
context 'payload is too large' do
let(:body_limit) { Gitlab::Middleware::CompressedJson::MAXIMUM_BODY_SIZE }
let(:decompressed_input) { 'a' * (body_limit + 100) }
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index bc1d53b2ccb..bed43c04460 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -278,7 +278,7 @@ RSpec.describe Gitlab::Middleware::Go do
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/other_markup_spec.rb b/spec/lib/gitlab/other_markup_spec.rb
index 26e60251abb..6b24c8a8710 100644
--- a/spec/lib/gitlab/other_markup_spec.rb
+++ b/spec/lib/gitlab/other_markup_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::OtherMarkup do
let(:context) { {} }
- context "XSS Checks" do
+ context 'XSS Checks' do
links = {
'links' => {
file: 'file.rdoc',
@@ -20,6 +20,33 @@ RSpec.describe Gitlab::OtherMarkup do
end
end
+ context 'when rendering takes too long' do
+ let_it_be(:file_name) { 'foo.bar' }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:context) { { project: project } }
+ let_it_be(:text) { +'Noël' }
+
+ before do
+ stub_const('Gitlab::OtherMarkup::RENDER_TIMEOUT', 0.1)
+ allow(GitHub::Markup).to receive(:render) do
+ sleep(0.2)
+ text
+ end
+ end
+
+ it 'times out' do
+ # expect twice because of timeout in SyntaxHighlightFilter
+ expect(Gitlab::RenderTimeout).to receive(:timeout).twice.and_call_original
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(Timeout::Error),
+ project_id: context[:project].id, file_name: file_name,
+ class_name: described_class.name.demodulize
+ )
+
+ expect(render(file_name, text, context)).to eq("<p>#{text}</p>")
+ end
+ end
+
def render(*args)
described_class.render(*args)
end
diff --git a/spec/lib/gitlab/pages/cache_control_spec.rb b/spec/lib/gitlab/pages/cache_control_spec.rb
index 431c989e874..d46124e0e16 100644
--- a/spec/lib/gitlab/pages/cache_control_spec.rb
+++ b/spec/lib/gitlab/pages/cache_control_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Pages::CacheControl do
+RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do
describe '.for_namespace' do
subject(:cache_control) { described_class.for_namespace(1) }
@@ -11,8 +11,14 @@ RSpec.describe Gitlab::Pages::CacheControl do
describe '#clear_cache' do
it 'clears the cache' do
expect(Rails.cache)
- .to receive(:delete)
- .with(/pages_domain_for_namespace_1_*/)
+ .to receive(:delete_multi)
+ .with(
+ array_including(
+ [
+ "pages_domain_for_namespace_1",
+ /pages_domain_for_namespace_1_*/
+ ]
+ ))
subject.clear_cache
end
@@ -27,8 +33,14 @@ RSpec.describe Gitlab::Pages::CacheControl do
describe '#clear_cache' do
it 'clears the cache' do
expect(Rails.cache)
- .to receive(:delete)
- .with(/pages_domain_for_project_1_*/)
+ .to receive(:delete_multi)
+ .with(
+ array_including(
+ [
+ "pages_domain_for_project_1",
+ /pages_domain_for_project_1_*/
+ ]
+ ))
subject.clear_cache
end
@@ -58,6 +70,14 @@ RSpec.describe Gitlab::Pages::CacheControl do
expect(described_class.new(type: :project, id: 1).cache_key).not_to eq(cache_key)
end
+
+ it 'caches the application settings hash' do
+ expect(Rails.cache)
+ .to receive(:write)
+ .with("pages_domain_for_project_1", kind_of(Set))
+
+ described_class.new(type: :project, id: 1).cache_key
+ end
end
it 'fails with invalid type' do
diff --git a/spec/lib/gitlab/pagination/offset_pagination_spec.rb b/spec/lib/gitlab/pagination/offset_pagination_spec.rb
index ebbd207cc11..b1c4ffd6c29 100644
--- a/spec/lib/gitlab/pagination/offset_pagination_spec.rb
+++ b/spec/lib/gitlab/pagination/offset_pagination_spec.rb
@@ -101,6 +101,28 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do
end
end
+ context 'when without_count is true' do
+ it_behaves_like 'paginated response'
+
+ it 'does not return the X-Total and X-Total-Pages headers' do
+ expect_no_header('X-Total')
+ expect_no_header('X-Total-Pages')
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Page', '1')
+ expect_header('X-Next-Page', '2')
+ expect_header('X-Prev-Page', '')
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
+ expect(val).not_to include('rel="last"')
+ expect(val).not_to include('rel="prev"')
+ end
+
+ expect { subject.paginate(resource, without_count: true) }.to make_queries_matching(/SELECT COUNT/, 0)
+ end
+ end
+
it 'does not return the total headers when excluding them' do
expect_no_header('X-Total')
expect_no_header('X-Total-Pages')
diff --git a/spec/lib/gitlab/process_management_spec.rb b/spec/lib/gitlab/process_management_spec.rb
index a71a476b540..fbd39702efb 100644
--- a/spec/lib/gitlab/process_management_spec.rb
+++ b/spec/lib/gitlab/process_management_spec.rb
@@ -41,15 +41,6 @@ RSpec.describe Gitlab::ProcessManagement do
end
end
- describe '.wait_async' do
- it 'waits for a process in a separate thread' do
- thread = described_class.wait_async(Process.spawn('true'))
-
- # Upon success Process.wait just returns the PID.
- expect(thread.value).to be_a_kind_of(Numeric)
- end
- end
-
# In the X_alive? checks, we check negative PIDs sometimes as a simple way
# to be sure the pids are definitely for non-existent processes.
# Note that -1 is special, and sends the signal to every process we have permission
diff --git a/spec/lib/gitlab/process_supervisor_spec.rb b/spec/lib/gitlab/process_supervisor_spec.rb
index 8356197805c..18de5053362 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 do
+RSpec.describe Gitlab::ProcessSupervisor, feature_category: :application_performance do
let(:health_check_interval_seconds) { 0.1 }
let(:check_terminate_interval_seconds) { 1 }
let(:forwarded_signals) { [] }
diff --git a/spec/lib/gitlab/puma_logging/json_formatter_spec.rb b/spec/lib/gitlab/puma_logging/json_formatter_spec.rb
index 64ace09e01b..d38f54bccf1 100644
--- a/spec/lib/gitlab/puma_logging/json_formatter_spec.rb
+++ b/spec/lib/gitlab/puma_logging/json_formatter_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe Gitlab::PumaLogging::JSONFormatter do
it "generate json format with timestamp and pid" do
- Timecop.freeze( Time.utc(2019, 12, 04, 9, 10, 11, 123456)) do
- expect(subject.call('log message')).to eq "{\"timestamp\":\"2019-12-04T09:10:11.123Z\",\"pid\":#{Process.pid},\"message\":\"log message\"}"
+ travel_to(Time.utc(2019, 12, 04, 9, 10, 11)) do
+ expect(subject.call('log message')).to eq "{\"timestamp\":\"2019-12-04T09:10:11.000Z\",\"pid\":#{Process.pid},\"message\":\"log message\"}"
end
end
end
diff --git a/spec/lib/gitlab/quick_actions/dsl_spec.rb b/spec/lib/gitlab/quick_actions/dsl_spec.rb
index 942d347424f..c0469537c68 100644
--- a/spec/lib/gitlab/quick_actions/dsl_spec.rb
+++ b/spec/lib/gitlab/quick_actions/dsl_spec.rb
@@ -3,9 +3,10 @@
require 'spec_helper'
RSpec.describe Gitlab::QuickActions::Dsl do
- before :all do
- DummyClass = Struct.new(:project) do
- include Gitlab::QuickActions::Dsl
+ before do
+ stub_const('DummyClass', Struct.new(:project))
+ DummyClass.class_eval do
+ include Gitlab::QuickActions::Dsl # rubocop:disable RSpec/DescribedClass
desc 'A command with no args'
command :no_args, :none do
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
index 207fe28e84e..0e7eedf66b1 100644
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ b/spec/lib/gitlab/redis/multi_store_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Redis::MultiStore do
+RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
using RSpec::Parameterized::TableSyntax
let_it_be(:redis_store_class) do
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 0ee8c35ae81..4d608c07736 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -262,8 +262,11 @@ RSpec.describe Gitlab::ReferenceExtractor do
describe '#all' do
let(:issue) { create(:issue, project: project) }
+ let(:issue2) { create(:issue, project: project) }
+ let(:issue2_url) { Rails.application.routes.url_helpers.project_issue_url(project, issue2) }
let(:label) { create(:label, project: project) }
- let(:text) { "Ref. #{issue.to_reference} and #{label.to_reference}" }
+ let(:alert) { create(:alert_management_alert, project: project) }
+ let(:text) { "Ref. #{issue.to_reference} and #{label.to_reference} and #{alert.to_reference} and #{issue2_url}" }
before do
project.add_developer(project.creator)
@@ -271,7 +274,22 @@ RSpec.describe Gitlab::ReferenceExtractor do
end
it 'returns all referables' do
- expect(subject.all).to match_array([issue, label])
+ expect(subject.all).to match_array([issue, label, alert, issue2])
+ end
+ end
+
+ describe '#alerts' do
+ let(:alert1) { create(:alert_management_alert, project: project) }
+ let(:alert2) { create(:alert_management_alert, project: project) }
+ let(:text) { "Alert ref: #{alert1.to_reference} URL: #{alert2.details_url} Infalid ref: ^alert#0" }
+
+ before do
+ project.add_developer(project.creator)
+ subject.analyze(text)
+ end
+
+ it 'returns alert referables' do
+ expect(subject.alerts).to match_array([alert1, alert2])
end
end
diff --git a/spec/lib/gitlab/repository_archive_rate_limiter_spec.rb b/spec/lib/gitlab/repository_archive_rate_limiter_spec.rb
index 49df70f3cb3..4599c647d5c 100644
--- a/spec/lib/gitlab/repository_archive_rate_limiter_spec.rb
+++ b/spec/lib/gitlab/repository_archive_rate_limiter_spec.rb
@@ -7,8 +7,7 @@ RSpec.describe ::Gitlab::RepositoryArchiveRateLimiter do
Class.new do
include ::Gitlab::RepositoryArchiveRateLimiter
- def check_rate_limit!(**args)
- end
+ def check_rate_limit!(**args); end
end
end
diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb
index d14c3f44c6f..71a20cc58fd 100644
--- a/spec/lib/gitlab/repository_cache_adapter_spec.rb
+++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb
@@ -27,8 +27,7 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
'foo/bar'
end
- def project
- end
+ def project; end
def cached_methods
[:letters]
diff --git a/spec/lib/gitlab/search/found_blob_spec.rb b/spec/lib/gitlab/search/found_blob_spec.rb
index 8b1c91f689d..c41a051bc42 100644
--- a/spec/lib/gitlab/search/found_blob_spec.rb
+++ b/spec/lib/gitlab/search/found_blob_spec.rb
@@ -141,9 +141,8 @@ RSpec.describe Gitlab::Search::FoundBlob do
subject { described_class.new(blob_path: path, project: project, ref: 'master') }
before do
- allow(Gitlab::Git::Blob).to receive(:batch).and_return([
- Gitlab::Git::Blob.new(path: path)
- ])
+ allow(Gitlab::Git::Blob)
+ .to receive(:batch).and_return([Gitlab::Git::Blob.new(path: path)])
end
it { expect(subject.path).to eq('a/b/c.md') }
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 785429aa3b0..049b8d4ed86 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -88,18 +88,11 @@ RSpec.describe Gitlab::Shell do
let(:disk_path) { "#{project.disk_path}.git" }
it 'returns true when the command succeeds' do
- expect(TestEnv.storage_dir_exists?(project.repository_storage, disk_path)).to be(true)
+ expect(project.repository.raw).to exist
expect(gitlab_shell.remove_repository(project.repository_storage, project.disk_path)).to be(true)
- expect(TestEnv.storage_dir_exists?(project.repository_storage, disk_path)).to be(false)
- end
-
- it 'keeps the namespace directory' do
- gitlab_shell.remove_repository(project.repository_storage, project.disk_path)
-
- expect(TestEnv.storage_dir_exists?(project.repository_storage, disk_path)).to be(false)
- expect(TestEnv.storage_dir_exists?(project.repository_storage, project.disk_path.gsub(project.name, ''))).to be(true)
+ expect(project.repository.raw).not_to exist
end
end
@@ -107,21 +100,22 @@ RSpec.describe Gitlab::Shell do
let!(:project2) { create(:project, :repository) }
it 'returns true when the command succeeds' do
- old_path = project2.disk_path
+ old_repo = project2.repository.raw
new_path = "project/new_path"
+ new_repo = Gitlab::Git::Repository.new(project2.repository_storage, "#{new_path}.git", nil, nil)
- expect(TestEnv.storage_dir_exists?(project2.repository_storage, "#{old_path}.git")).to be(true)
- expect(TestEnv.storage_dir_exists?(project2.repository_storage, "#{new_path}.git")).to be(false)
+ expect(old_repo).to exist
+ expect(new_repo).not_to exist
- expect(gitlab_shell.mv_repository(project2.repository_storage, old_path, new_path)).to be_truthy
+ expect(gitlab_shell.mv_repository(project2.repository_storage, project2.disk_path, new_path)).to be_truthy
- expect(TestEnv.storage_dir_exists?(project2.repository_storage, "#{old_path}.git")).to be(false)
- expect(TestEnv.storage_dir_exists?(project2.repository_storage, "#{new_path}.git")).to be(true)
+ expect(old_repo).not_to exist
+ expect(new_repo).to exist
end
it 'returns false when the command fails' do
expect(gitlab_shell.mv_repository(project2.repository_storage, project2.disk_path, '')).to be_falsy
- expect(TestEnv.storage_dir_exists?(project2.repository_storage, "#{project2.disk_path}.git")).to be(true)
+ expect(project2.repository.raw).to exist
end
end
end
@@ -133,9 +127,11 @@ RSpec.describe Gitlab::Shell do
describe '#add_namespace' do
it 'creates a namespace' do
- Gitlab::GitalyClient::NamespaceService.allow { subject.add_namespace(storage, "mepmep") }
+ Gitlab::GitalyClient::NamespaceService.allow do
+ subject.add_namespace(storage, "mepmep")
- expect(TestEnv.storage_dir_exists?(storage, "mepmep")).to be(true)
+ expect(Gitlab::GitalyClient::NamespaceService.new(storage).exists?("mepmep")).to be(true)
+ end
end
end
@@ -160,9 +156,9 @@ RSpec.describe Gitlab::Shell do
Gitlab::GitalyClient::NamespaceService.allow do
subject.add_namespace(storage, "mepmep")
subject.rm_namespace(storage, "mepmep")
- end
- expect(TestEnv.storage_dir_exists?(storage, "mepmep")).to be(false)
+ expect(Gitlab::GitalyClient::NamespaceService.new(storage).exists?("mepmep")).to be(false)
+ end
end
end
@@ -171,10 +167,10 @@ RSpec.describe Gitlab::Shell do
Gitlab::GitalyClient::NamespaceService.allow do
subject.add_namespace(storage, "mepmep")
subject.mv_namespace(storage, "mepmep", "2mep")
- end
- expect(TestEnv.storage_dir_exists?(storage, "mepmep")).to be(false)
- expect(TestEnv.storage_dir_exists?(storage, "2mep")).to be(true)
+ expect(Gitlab::GitalyClient::NamespaceService.new(storage).exists?("mepmep")).to be(false)
+ expect(Gitlab::GitalyClient::NamespaceService.new(storage).exists?("2mep")).to be(true)
+ end
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
index 8c9a1abba5a..5baeec93036 100644
--- a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
+++ b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
@@ -4,11 +4,23 @@ require 'spec_helper'
RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
let(:memory_killer) { described_class.new }
+ let(:sidekiq_daemon_monitor) { instance_double(Gitlab::SidekiqDaemon::Monitor) }
+ let(:running_jobs) { {} }
let(:pid) { 12345 }
+ let(:worker) do
+ Class.new do
+ def self.name
+ 'DummyWorker'
+ end
+ end
+ end
before do
+ stub_const('DummyWorker', worker)
allow(Sidekiq.logger).to receive(:info)
allow(Sidekiq.logger).to receive(:warn)
+ allow(Gitlab::SidekiqDaemon::Monitor).to receive(:instance).and_return(sidekiq_daemon_monitor)
+ allow(sidekiq_daemon_monitor).to receive(:jobs).and_return(running_jobs)
allow(memory_killer).to receive(:pid).and_return(pid)
# make sleep no-op
@@ -306,31 +318,37 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
stub_const("#{described_class}::CHECK_INTERVAL_SECONDS", check_interval_seconds)
end
- it 'send signal and return when all jobs finished' do
- expect(Process).to receive(:kill).with(signal, pid).ordered
- expect(Gitlab::Metrics::System).to receive(:monotonic_time).and_call_original
+ context 'when all jobs are finished' do
+ let(:running_jobs) { {} }
- expect(memory_killer).to receive(:enabled?).and_return(true)
- expect(memory_killer).to receive(:any_jobs?).and_return(false)
+ it 'send signal and return when all jobs finished' do
+ expect(Process).to receive(:kill).with(signal, pid).ordered
+ expect(Gitlab::Metrics::System).to receive(:monotonic_time).and_call_original
- expect(memory_killer).not_to receive(:sleep)
+ expect(memory_killer).to receive(:enabled?).and_return(true)
- subject
+ expect(memory_killer).not_to receive(:sleep)
+
+ subject
+ end
end
- it 'send signal and wait till deadline if any job not finished' do
- expect(Process).to receive(:kill)
- .with(signal, pid)
- .ordered
+ context 'when there are still running jobs' do
+ let(:running_jobs) { { 'jid1' => { worker_class: DummyWorker } } }
- expect(Gitlab::Metrics::System).to receive(:monotonic_time)
- .and_call_original
- .at_least(:once)
+ it 'send signal and wait till deadline if any job not finished' do
+ expect(Process).to receive(:kill)
+ .with(signal, pid)
+ .ordered
+
+ expect(Gitlab::Metrics::System).to receive(:monotonic_time)
+ .and_call_original
+ .at_least(:once)
- expect(memory_killer).to receive(:enabled?).and_return(true).at_least(:once)
- expect(memory_killer).to receive(:any_jobs?).and_return(true).at_least(:once)
+ expect(memory_killer).to receive(:enabled?).and_return(true).at_least(:once)
- subject
+ subject
+ end
end
end
@@ -377,21 +395,11 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
let(:jid) { 1 }
let(:reason) { 'rss out of range reason description' }
let(:queue) { 'default' }
- let(:running_jobs) { [{ jid: jid, worker_class: 'DummyWorker' }] }
- let(:metrics) { memory_killer.instance_variable_get(:@metrics) }
- let(:worker) do
- Class.new do
- def self.name
- 'DummyWorker'
- end
- include ApplicationWorker
- end
- end
+ let(:metrics) { memory_killer.instance_variable_get(:@metrics) }
+ let(:running_jobs) { { jid => { worker_class: DummyWorker } } }
before do
- stub_const("DummyWorker", worker)
-
allow(memory_killer).to receive(:get_rss_kb).and_return(*current_rss)
allow(memory_killer).to receive(:get_soft_limit_rss_kb).and_return(soft_limit_rss)
allow(memory_killer).to receive(:get_hard_limit_rss_kb).and_return(hard_limit_rss)
@@ -413,15 +421,13 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
hard_limit_rss: hard_limit_rss,
soft_limit_rss: soft_limit_rss,
reason: reason,
- running_jobs: running_jobs,
+ running_jobs: [jid: jid, worker_class: 'DummyWorker'],
memory_total_kb: memory_total)
expect(metrics[:sidekiq_memory_killer_running_jobs]).to receive(:increment)
.with({ worker_class: "DummyWorker", deadline_exceeded: true })
- Gitlab::SidekiqDaemon::Monitor.instance.within_job(DummyWorker, jid, queue) do
- subject
- end
+ subject
end
end
@@ -452,6 +458,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
expect(subject).to eq("current_rss(#{rss}) > soft_limit_rss(#{soft_limit}) longer than GRACE_BALLOON_SECONDS(#{grace_balloon_seconds})")
end
end
+
context 'deadline not exceeded' do
let(:deadline_exceeded) { false }
@@ -463,21 +470,24 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
end
describe '#rss_increase_by_jobs' do
- let(:running_jobs) { { id1: 'job1', id2: 'job2' } }
+ let(:running_jobs) { { 'job1' => { worker_class: "Job1" }, 'job2' => { worker_class: "Job2" } } }
subject { memory_killer.send(:rss_increase_by_jobs) }
+ before do
+ allow(memory_killer).to receive(:rss_increase_by_job).and_return(11, 22)
+ end
+
it 'adds up individual rss_increase_by_job' do
- allow(Gitlab::SidekiqDaemon::Monitor).to receive_message_chain(:instance, :jobs_mutex, :synchronize).and_yield
- expect(Gitlab::SidekiqDaemon::Monitor).to receive_message_chain(:instance, :jobs).and_return(running_jobs)
- expect(memory_killer).to receive(:rss_increase_by_job).and_return(11, 22)
expect(subject).to eq(33)
end
- it 'return 0 if no job' do
- allow(Gitlab::SidekiqDaemon::Monitor).to receive_message_chain(:instance, :jobs_mutex, :synchronize).and_yield
- expect(Gitlab::SidekiqDaemon::Monitor).to receive_message_chain(:instance, :jobs).and_return({})
- expect(subject).to eq(0)
+ context 'when there is no running job' do
+ let(:running_jobs) { {} }
+
+ it 'return 0 if no job' do
+ expect(subject).to eq(0)
+ end
end
end
diff --git a/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb b/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb
index f93c0e28fc0..479ef29bbf9 100644
--- a/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb
+++ b/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb
@@ -6,9 +6,15 @@ RSpec.describe Gitlab::SidekiqDaemon::Monitor do
let(:monitor) { described_class.new }
describe '#within_job' do
- it 'tracks thread' do
+ it 'tracks thread, jid and worker_class' do
blk = proc do
- expect(monitor.jobs.dig('jid', :thread)).not_to be_nil
+ monitor.jobs do |jobs|
+ jobs.each do |jid, job|
+ expect(job[:thread]).not_to be_nil
+ expect(jid).to eq('jid')
+ expect(job[:worker_class]).to eq('worker_class')
+ end
+ end
"OK"
end
@@ -37,6 +43,17 @@ RSpec.describe Gitlab::SidekiqDaemon::Monitor do
end
end
+ describe '#jobs' do
+ it 'returns running jobs hash' do
+ jid = SecureRandom.hex
+ running_jobs = { jid => hash_including(worker_class: 'worker_class') }
+
+ monitor.within_job('worker_class', jid, 'queue') do
+ expect(monitor.jobs).to match(running_jobs)
+ end
+ end
+ end
+
describe '#run_thread when notification channel not enabled' do
subject { monitor.send(:run_thread) }
@@ -220,7 +237,7 @@ RSpec.describe Gitlab::SidekiqDaemon::Monitor do
let(:thread) { Thread.new { sleep 1000 } }
before do
- monitor.jobs[jid] = { worker_class: 'worker_class', thread: thread, started_at: Time.now.to_i }
+ allow(monitor).to receive(:find_thread_unsafe).with(jid).and_return(thread)
end
after do
diff --git a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
index dca00c85e30..472591bde5e 100644
--- a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
@@ -40,8 +40,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ClientMetrics do
TestWorker.class_eval do
include Sidekiq::Worker
- def perform(*args)
- end
+ def perform(*args); end
end
allow(Gitlab::Metrics).to receive(:counter).and_return(Gitlab::Metrics::NullMetric.instance)
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb
index 44c8df73463..14eb568b974 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb
@@ -17,8 +17,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Client, :clean_gitlab_r
include ApplicationWorker
- def perform(*args)
- end
+ def perform(*args); end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb
index 09548d21106..1b01793d80d 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb
@@ -18,8 +18,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Server, :clean_gitlab_r
self.class.work
end
- def self.work
- end
+ def self.work; end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb b/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb
index 8cf65e1be5b..cfb2c7ab5c3 100644
--- a/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb
@@ -13,8 +13,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::InstrumentationLogger do
include ApplicationWorker
- def perform(*args)
- end
+ def perform(*args); end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/query_analyzer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/query_analyzer_spec.rb
index e58af1d60fe..c31f05f00e4 100644
--- a/spec/lib/gitlab/sidekiq_middleware/query_analyzer_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/query_analyzer_spec.rb
@@ -10,8 +10,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::QueryAnalyzer, query_analyzers: false
let(:queue) { 'some-queue' }
let(:middleware) { described_class.new }
- def do_queries
- end
+ def do_queries; end
subject { middleware.call(worker, job, queue) { do_queries } }
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index 1a53a9b8701..f7cee6beb58 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -231,8 +231,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
include Sidekiq::Worker
include WorkerAttributes
- def perform(*args)
- end
+ def perform(*args); end
end
allow(::Gitlab::Database::LoadBalancing).to receive_message_chain(:proxy, :load_balancer).and_return(load_balancer)
@@ -306,8 +305,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
feature_category :not_owned
end
- def perform
- end
+ def perform; 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 821d8b8fe7b..1b6cd7ac5fb 100644
--- a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
@@ -17,8 +17,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
jobs.find { |job| job['args'] == args }
end
- def perform(*args)
- end
+ def perform(*args); end
end
end
@@ -38,8 +37,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
'TestMailer'
end
- def test_mail
- end
+ def test_mail; end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb
index 05b328e55d3..2deab3064eb 100644
--- a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb
@@ -41,8 +41,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Server do
include Sidekiq::Worker
- def perform
- end
+ def perform; end
end
end
diff --git a/spec/lib/gitlab/slash_commands/application_help_spec.rb b/spec/lib/gitlab/slash_commands/application_help_spec.rb
index b182c0e5cc6..d0cefdf4895 100644
--- a/spec/lib/gitlab/slash_commands/application_help_spec.rb
+++ b/spec/lib/gitlab/slash_commands/application_help_spec.rb
@@ -4,13 +4,11 @@ require 'spec_helper'
RSpec.describe Gitlab::SlashCommands::ApplicationHelp do
let(:params) { { command: '/gitlab', text: 'help' } }
- let_it_be(:user) { create(:user) }
- let_it_be(:chat_user) { create(:chat_name, user: user) }
let(:project) { build(:project) }
describe '#execute' do
subject do
- described_class.new(project, chat_user, params).execute
+ described_class.new(project, params).execute
end
it 'displays the help section' do
diff --git a/spec/lib/gitlab/slash_commands/deploy_spec.rb b/spec/lib/gitlab/slash_commands/deploy_spec.rb
index 5af234ff88e..94a95fb417f 100644
--- a/spec/lib/gitlab/slash_commands/deploy_spec.rb
+++ b/spec/lib/gitlab/slash_commands/deploy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::SlashCommands::Deploy do
+RSpec.describe Gitlab::SlashCommands::Deploy, feature_category: :team_planning do
describe '#execute' do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb
index 60bb006673f..a34ddf8773c 100644
--- a/spec/lib/gitlab/sql/pattern_spec.rb
+++ b/spec/lib/gitlab/sql/pattern_spec.rb
@@ -210,7 +210,7 @@ RSpec.describe Gitlab::SQL::Pattern do
let(:query) { 'foo' }
it 'returns a single ILIKE condition' do
- expect(fuzzy_arel_match.to_sql).to match(/title.*I?LIKE '\%foo\%'/)
+ expect(fuzzy_arel_match.to_sql).to match(/title.*I?LIKE '%foo%'/)
end
end
@@ -232,7 +232,7 @@ RSpec.describe Gitlab::SQL::Pattern do
let(:query) { 'foo baz' }
it 'returns a joining LIKE condition using a AND' do
- expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%'/)
+ expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '%foo%' AND .*title.*I?LIKE '%baz%'/)
end
end
@@ -248,7 +248,7 @@ RSpec.describe Gitlab::SQL::Pattern do
let(:query) { 'foo ba' }
it 'returns a single ILIKE condition using the longer word' do
- expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '\%foo\%'/)
+ expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '%foo%'/)
end
end
@@ -256,7 +256,7 @@ RSpec.describe Gitlab::SQL::Pattern do
let(:query) { 'foo "really bar" baz' }
it 'returns a joining LIKE condition using a AND' do
- expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%' AND .*title.*I?LIKE '\%really bar\%'/)
+ expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '%foo%' AND .*title.*I?LIKE '%baz%' AND .*title.*I?LIKE '%really bar%'/)
end
end
@@ -266,7 +266,7 @@ RSpec.describe Gitlab::SQL::Pattern do
subject(:fuzzy_arel_match) { Project.fuzzy_arel_match(Route.arel_table[:path], query) }
it 'returns a condition with the table and column name' do
- expect(fuzzy_arel_match.to_sql).to match(/"routes"."path".*ILIKE '\%foo\%'/)
+ expect(fuzzy_arel_match.to_sql).to match(/"routes"."path".*ILIKE '%foo%'/)
end
end
end
diff --git a/spec/lib/gitlab/ssh/signature_spec.rb b/spec/lib/gitlab/ssh/signature_spec.rb
index e8d366f0762..5149972dbf9 100644
--- a/spec/lib/gitlab/ssh/signature_spec.rb
+++ b/spec/lib/gitlab/ssh/signature_spec.rb
@@ -5,20 +5,20 @@ require 'spec_helper'
RSpec.describe Gitlab::Ssh::Signature do
# ssh-keygen -t ed25519
let_it_be(:committer_email) { 'ssh-commit-test@example.com' }
- let_it_be(:public_key_text) { 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJKOfqOH0fDde+Ua/1SObkXB1CEDF5M6UfARMpW3F87u' }
+ let_it_be(:public_key_text) { 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHZ8NHEnCIpC4mnot+BRxv6L+fq+TnN1CgsRrHWLmfwb' }
let_it_be_with_reload(:user) { create(:user, email: committer_email) }
- let_it_be_with_reload(:key) { create(:key, key: public_key_text, user: user) }
+ let_it_be_with_reload(:key) { create(:key, usage_type: :signing, key: public_key_text, user: user) }
let(:signed_text) { 'This message was signed by an ssh key' }
let(:signature_text) do
- # ssh-keygen -Y sign -n file -f id_test message.txt
+ # ssh-keygen -Y sign -n git -f id_test message.txt
<<~SIG
-----BEGIN SSH SIGNATURE-----
- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgko5+o4fR8N175Rr/VI5uRcHUIQ
- MXkzpR8BEylbcXzu4AAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
- OQAAAECQa95KgBkgbMwIPNwHRjHu0WYrKvAc5O/FaBXlTDcPWQHi8WRDhbPNN6MqSYLg/S
- hsei6Y8VYPv85StrEHYdoF
+ U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgdnw0cScIikLiaei34FHG/ov5+r
+ 5Oc3UKCxGsdYuZ/BsAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
+ AAAAQDWOEauf0jXyA9caa5bOgK5QZD6c69pm+EbG3GMw5QBL3N/Gt+r413McCSJFohWWBk
+ Lxemg8NzZ0nB7lTFbaxQc=
-----END SSH SIGNATURE-----
SIG
end
@@ -51,37 +51,37 @@ RSpec.describe Gitlab::Ssh::Signature do
context 'when using an RSA key' do
let(:public_key_text) do
<<~KEY.delete("\n")
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCr3ucg9tLf87S2TxgeDaO4Cs5Mzv7wwi5w
- OnSG8hE/Zj7xzf0kXAYns/dHhPilkQMCulMQuGGprGzDJXZ9WrrVDHgBj2+kLB8cc+XYIb29
- HPsoz5a1T776wWrzs5cw3Vbb0ZEMPG27SfJ+HtIqnIAcgBoRxgP/+I9we7tVxrTuog/9jSzU
- H1IscwfwgKdUrvN5cyhqqxWspwZVlf6s4jaVjC9sKlF7u9CBCxqM2G7GZRKH2sEV2Tw0mT4z
- 39UQ5uz9+4hxWChosiQChrT9zSJDGWQm3WGn5ubYPeB/xINEKkFxuEupnSK7l8PQxeLAwlcN
- YHKMkHdO16O6PlpxvcLR1XVy4F12NXCxFjTr8GmFvJTvevf9iuFRmYQpffqm+EMN0shuhPag
- Z1poVK7ZMO49b4HD6csGwDjXEgNAnyi7oPV1WMHVy+xi2j+yaAgiVk50kgTwp9sGkHTiMTM8
- YWjCq+Hb+HXLINmqO5V1QChT7PAFYycmQ0Fe2x39eLLMHy0=
+ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDkq6ko8LMxf2NwyJKh+77KSDc7/ynPgUJD
+ IopkhqftuHFYe2Y+V3MBJnpzfSRwR2xGfXQUUzLU9AGyfZIO/ZLK2yvfhlO3k//5PbAaZb3y
+ urlnF9T1d2nhtfi8wuzsEn7Boh6qdoWPFIsloAL/X0PXH1HWKmzyNer92HKGrnWFfaaEMo0n
+ T3ureAhRG4IONyUcOK+DyoH+YbxXSlHnLO2oHHlWaP9RrJCHbfAQbfDhaZCI0cNkXXOwUwA4
+ yWGzDibfXZTvaYxpjbz1xoHmCAq8IrobCgkQaEg3PH3vPGnbP0TpViXjMnZyBZyT7tg9WHBV
+ kAsl0CizyUgZHPAPYuqKy5JNlnjVjeqYeIgdN4Tj7hpJ1n0hVpRk4zQNYRmAAj3GNqgPAsd0
+ 3i4rW8cqmhO0fmhP5DgQ7Mt5S9AgcTcCr6niPacK34XrwKiRjxXmCLjr36q8wuRU3QdMt+MK
+ Zxk/qJdAUIltz+nuGiwct0w+sWefYzmiRXu6hljBBrRAvnU=
KEY
end
let(:signature_text) do
<<~SIG
-----BEGIN SSH SIGNATURE-----
- U1NIU0lHAAAAAQAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAKve5yD20t/ztLZPGB4No7
- gKzkzO/vDCLnA6dIbyET9mPvHN/SRcBiez90eE+KWRAwK6UxC4YamsbMMldn1autUMeAGP
- b6QsHxxz5dghvb0c+yjPlrVPvvrBavOzlzDdVtvRkQw8bbtJ8n4e0iqcgByAGhHGA//4j3
- B7u1XGtO6iD/2NLNQfUixzB/CAp1Su83lzKGqrFaynBlWV/qziNpWML2wqUXu70IELGozY
- bsZlEofawRXZPDSZPjPf1RDm7P37iHFYKGiyJAKGtP3NIkMZZCbdYafm5tg94H/Eg0QqQX
- G4S6mdIruXw9DF4sDCVw1gcoyQd07Xo7o+WnG9wtHVdXLgXXY1cLEWNOvwaYW8lO969/2K
- 4VGZhCl9+qb4Qw3SyG6E9qBnWmhUrtkw7j1vgcPpywbAONcSA0CfKLug9XVYwdXL7GLaP7
- JoCCJWTnSSBPCn2waQdOIxMzxhaMKr4dv4dcsg2ao7lXVAKFPs8AVjJyZDQV7bHf14sswf
- LQAAAARmaWxlAAAAAAAAAAZzaGE1MTIAAAGUAAAADHJzYS1zaGEyLTUxMgAAAYAXgXpXWw
- A1fYHTUON+e1yrTw8AKB4ymfqpR9Zr1OUmYUKJ9xXvvyNCfKHL6XD14CkMu1Tx8Z3TTPG9
- C6uAXBniKRwwaLVOKffZMshf5sbjcy65KkqBPC7n/cDiCAeoJ8Y05trEDV62+pOpB2lLdv
- pwwg2o0JaoLbdRcKCD0pw1u0O7VDDngTKFZ4ghHrEslxwlFruht1h9hs3rmdITlT0RMNuU
- PHGAIB56u4E4UeoMd3D5rga+4Boj0s6551VgP3vCmcz9ZojPHhTCQdUZU1yHdEBTadYTq6
- UWHhQwDCUDkSNKCRxWo6EyKZQeTakedAt4qkdSpSUCKOJGWKmPOfAm2/sDEmSxffRdxRRg
- QUe8lklyFTZd6U/ZkJ/y7VR46fcSkEqLSLd9jAZT/3HJXbZfULpwsTcvcLcJLkCuzHEaU1
- LRyJBsanLCYHTv7ep5PvIuAngUWrXK2eb7oacVs94mWXfs1PG482Ym4+bZA5u0QliGTVaC
- M2EMhRTf0cqFuA4=
+ U1NIU0lHAAAAAQAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAOSrqSjwszF/Y3DIkqH7vs
+ pINzv/Kc+BQkMiimSGp+24cVh7Zj5XcwEmenN9JHBHbEZ9dBRTMtT0AbJ9kg79ksrbK9+G
+ U7eT//k9sBplvfK6uWcX1PV3aeG1+LzC7OwSfsGiHqp2hY8UiyWgAv9fQ9cfUdYqbPI16v
+ 3YcoaudYV9poQyjSdPe6t4CFEbgg43JRw4r4PKgf5hvFdKUecs7agceVZo/1GskIdt8BBt
+ 8OFpkIjRw2Rdc7BTADjJYbMOJt9dlO9pjGmNvPXGgeYICrwiuhsKCRBoSDc8fe88ads/RO
+ lWJeMydnIFnJPu2D1YcFWQCyXQKLPJSBkc8A9i6orLkk2WeNWN6ph4iB03hOPuGknWfSFW
+ lGTjNA1hGYACPcY2qA8Cx3TeLitbxyqaE7R+aE/kOBDsy3lL0CBxNwKvqeI9pwrfhevAqJ
+ GPFeYIuOvfqrzC5FTdB0y34wpnGT+ol0BQiW3P6e4aLBy3TD6xZ59jOaJFe7qGWMEGtEC+
+ dQAAAANnaXQAAAAAAAAABnNoYTUxMgAAAZQAAAAMcnNhLXNoYTItNTEyAAABgEnuYyYOlM
+ CSR+wvmBY7eKHzFor5ByM7N4F7VZAGKK/vbS3C38xDdiJZwsZUscpe5WspJVCWUTkFxXjn
+ GW7vseIfJBVkyqnu2uN8X1j/VDLFESEajcchPhPxtfAMK1/NL99O7rCrYX2pmpkm9tWsFk
+ NX5B93sRyDUnHAOkB+zdqU8P0xdzc8kmBl5OOqu1rSjZIgnQjcauEIRIUN+rFuiRRmIvJp
+ UvMhkKSsRCH93btGW7A6x5e4iPzP+Em0UFYJdOx2lvu9aVAktQzysGwDN+9c4IC+07UHKT
+ UIE5jSbR1QKfavcywNQnCltQ2bTxpnm4A6QHKcdr9Q57dV014FgtmtT/Pw03iyl5MwbEqW
+ 7YEHSkMyAcd1rjEpOCN2pJjjbrOKLePG0R2ffgvVJnTWGFklCxsJ1/7IASHst1wg1/gu1g
+ Kx/TEv+gOKpehAgs2Sz/4kZtFuHO2dbHYC3UrPR5HT8JnQWeCfiT0qwsVQ6xribw0jEYyd
+ ZBNWKkPdNocAbA==
-----END SSH SIGNATURE-----
SIG
end
@@ -98,10 +98,10 @@ RSpec.describe Gitlab::Ssh::Signature do
let(:signature_text) do
<<~SIG
-----BEGIN SSH SIGNATURE-----
- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgko5+o4fR8N175Rr/VI5uRcHUIQ
- MXkzpR8BEylbcXzu4AAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
- OQAAAEC1y2I7o3KqKFlnM+MLkhIo+uRX3YQOYCqycfibyfvmkZTcwqMxgNBInBM9pY3VvS
- sbW2iEdgz34agHbi+1BHIM
+ U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgdnw0cScIikLiaei34FHG/ov5+r
+ 5Oc3UKCxGsdYuZ/BsAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
+ AAAAQP2liwaQ44PC9oXf5Xzjq20WLdWEK9nyonvDGtduGUXMOL4yP5A6WvKz7kSt7Vba/U
+ MNK0nmnNc7Aokfh/2eRQE=
-----END SSH SIGNATURE-----
SIG
end
@@ -151,16 +151,32 @@ RSpec.describe Gitlab::Ssh::Signature do
context 'when user email is not verified' do
before do
+ email = user.emails.find_by(email: committer_email)
+ email.update!(confirmed_at: nil)
user.update!(confirmed_at: nil)
end
- it_behaves_like 'unverified signature'
+ it 'reports unverified status' do
+ expect(signature.verification_status).to eq(:unverified)
+ end
+ end
+
+ context 'when no user exist with the committer email' do
+ before do
+ user.delete
+ end
+
+ it 'reports other_user status' do
+ expect(signature.verification_status).to eq(:other_user)
+ end
end
context 'when no user exists with the committer email' do
let(:committer_email) { 'different-email+ssh-commit-test@example.com' }
- it_behaves_like 'unverified signature'
+ it 'reports other_user status' do
+ expect(signature.verification_status).to eq(:other_user)
+ end
end
context 'when signature is invalid' do
@@ -178,6 +194,21 @@ RSpec.describe Gitlab::Ssh::Signature do
it_behaves_like 'unverified signature'
end
+ context 'when signature is for a different namespace' do
+ let(:signature_text) do
+ <<~SIG
+ -----BEGIN SSH SIGNATURE-----
+ U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgdnw0cScIikLiaei34FHG/ov5+r
+ 5Oc3UKCxGsdYuZ/BsAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
+ OQAAAEAd6Psg4D/5IdSVTy35D4t2iNX4udJnX8JrUCjQl0GoPl1vzPjgyvxdzdoQl6bh1w
+ 4rror3RuzUYBGzIioIc1MP
+ -----END SSH SIGNATURE-----
+ SIG
+ end
+
+ it_behaves_like 'unverified signature'
+ end
+
context 'when signature is for a different message' do
let(:signature_text) do
<<~SIG
@@ -204,13 +235,25 @@ RSpec.describe Gitlab::Ssh::Signature do
it_behaves_like 'unverified signature'
end
- context 'when key does not exist in GitLab' do
- before do
- key.delete
+ context 'when the signing key does not exist in GitLab' do
+ context 'when the key is not a signing one' do
+ before do
+ key.auth!
+ end
+
+ it 'reports unknown_key status' do
+ expect(signature.verification_status).to eq(:unknown_key)
+ end
end
- it 'reports unknown_key status' do
- expect(signature.verification_status).to eq(:unknown_key)
+ context 'when the key is removed' do
+ before do
+ key.delete
+ end
+
+ it 'reports unknown_key status' do
+ expect(signature.verification_status).to eq(:unknown_key)
+ end
end
end
diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb
index a2524314458..d4b0b1ea53b 100644
--- a/spec/lib/gitlab/ssh_public_key_spec.rb
+++ b/spec/lib/gitlab/ssh_public_key_spec.rb
@@ -260,8 +260,7 @@ RSpec.describe Gitlab::SSHPublicKey, lib: true, fips_mode: false do
context 'when the key is represented by a subclass of the class that is in the list of supported technologies' do
it 'raises error' do
rsa_subclass = Class.new(described_class.technology(:rsa).key_class) do
- def initialize
- end
+ def initialize; end
end
key = rsa_subclass.new
diff --git a/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb b/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb
index 1d4725cf405..e79535358f9 100644
--- a/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb
+++ b/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb
@@ -3,8 +3,11 @@
require 'spec_helper'
RSpec.describe Gitlab::Tracking::Destinations::Snowplow, :do_not_stub_snowplow_by_default do
- let(:emitter) { SnowplowTracker::Emitter.new('localhost', buffer_size: 1) }
- let(:tracker) { SnowplowTracker::Tracker.new(emitter, SnowplowTracker::Subject.new, 'namespace', 'app_id') }
+ let(:emitter) { SnowplowTracker::Emitter.new(endpoint: 'localhost', options: { buffer_size: 1 }) }
+ let(:tracker) do
+ SnowplowTracker::Tracker.new(emitters: [emitter], subject: SnowplowTracker::Subject.new, namespace: 'namespace',
+ app_id: 'app_id')
+ end
before do
stub_application_setting(snowplow_collector_hostname: 'gitfoo.com')
@@ -21,16 +24,19 @@ RSpec.describe Gitlab::Tracking::Destinations::Snowplow, :do_not_stub_snowplow_b
expect(SnowplowTracker::AsyncEmitter)
.to receive(:new)
- .with('gitfoo.com',
- { protocol: 'https',
- on_success: subject.method(:increment_successful_events_emissions),
- on_failure: subject.method(:failure_callback) })
+ .with(endpoint: 'gitfoo.com',
+ options: { protocol: 'https',
+ on_success: subject.method(:increment_successful_events_emissions),
+ on_failure: subject.method(:failure_callback) })
.and_return(emitter)
expect(SnowplowTracker::Tracker)
.to receive(:new)
- .with(emitter, an_instance_of(SnowplowTracker::Subject), described_class::SNOWPLOW_NAMESPACE, '_abc123_')
- .and_return(tracker)
+ .with(emitters: [emitter],
+ subject: an_instance_of(SnowplowTracker::Subject),
+ namespace: described_class::SNOWPLOW_NAMESPACE,
+ app_id: '_abc123_')
+ .and_return(tracker)
end
describe '#event' do
@@ -41,7 +47,8 @@ RSpec.describe Gitlab::Tracking::Destinations::Snowplow, :do_not_stub_snowplow_b
expect(tracker)
.to have_received(:track_struct_event)
- .with('category', 'action', 'label', 'property', 1.5, nil, (Time.now.to_f * 1000).to_i)
+ .with(category: 'category', action: 'action', label: 'label', property: 'property', value: 1.5, context: nil,
+ tstamp: (Time.now.to_f * 1000).to_i)
end
it 'increase total snowplow events counter' do
diff --git a/spec/lib/gitlab/tracking/event_definition_spec.rb b/spec/lib/gitlab/tracking/event_definition_spec.rb
index 623009e9a30..c8e616b092b 100644
--- a/spec/lib/gitlab/tracking/event_definition_spec.rb
+++ b/spec/lib/gitlab/tracking/event_definition_spec.rb
@@ -83,6 +83,11 @@ RSpec.describe Gitlab::Tracking::EventDefinition do
subject { described_class.definitions }
+ after do
+ FileUtils.rm_rf(metric1)
+ FileUtils.rm_rf(metric2)
+ end
+
it 'has empty list when there are no definition files' do
is_expected.to be_empty
end
@@ -92,10 +97,5 @@ RSpec.describe Gitlab::Tracking::EventDefinition do
is_expected.to be_one
end
-
- after do
- FileUtils.rm_rf(metric1)
- FileUtils.rm_rf(metric2)
- end
end
end
diff --git a/spec/lib/gitlab/tracking/service_ping_context_spec.rb b/spec/lib/gitlab/tracking/service_ping_context_spec.rb
index d70dfaa4e0b..7530650b902 100644
--- a/spec/lib/gitlab/tracking/service_ping_context_spec.rb
+++ b/spec/lib/gitlab/tracking/service_ping_context_spec.rb
@@ -4,16 +4,55 @@ require 'spec_helper'
RSpec.describe Gitlab::Tracking::ServicePingContext do
describe '#init' do
- it 'does not accept unsupported data sources' do
- expect { described_class.new(data_source: :random, event: 'event a') }.to raise_error(ArgumentError)
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with valid configuration' do
+ where(:data_source, :event, :key_path) do
+ :redis | nil | 'counts.some_metric'
+ :redis_hll | 'some_event' | nil
+ end
+
+ with_them do
+ it 'does not raise errors' do
+ expect { described_class.new(data_source: data_source, event: event, key_path: key_path) }.not_to raise_error
+ end
+ end
+ end
+
+ context 'with invalid configuration' do
+ where(:data_source, :event, :key_path) do
+ :redis | nil | nil
+ :redis | 'some_event' | nil
+ :redis_hll | nil | nil
+ :redis_hll | nil | 'some key_path'
+ :random | 'some_event' | nil
+ end
+
+ with_them do
+ subject(:new_instance) { described_class.new(data_source: data_source, event: event, key_path: key_path) }
+
+ it 'does not raise errors' do
+ expect { new_instance }.to raise_error(ArgumentError)
+ end
+ end
end
end
describe '#to_context' do
- let(:subject) { described_class.new(data_source: :redis_hll, event: 'sample_event') }
+ context 'for redis_hll data source' do
+ let(:context_instance) { described_class.new(data_source: :redis_hll, event: 'sample_event') }
+
+ it 'contains event_name' do
+ expect(context_instance.to_context.to_json.dig(:data, :event_name)).to eq('sample_event')
+ end
+ end
+
+ context 'for redis data source' do
+ let(:context_instance) { described_class.new(data_source: :redis, key_path: 'counts.sample_metric') }
- it 'contains event_name' do
- expect(subject.to_context.to_json.dig(:data, :event_name)).to eq('sample_event')
+ it 'contains event_name' do
+ expect(context_instance.to_context.to_json.dig(:data, :key_path)).to eq('counts.sample_metric')
+ end
end
end
end
diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb
index e11175c776d..99ca402616a 100644
--- a/spec/lib/gitlab/tracking_spec.rb
+++ b/spec/lib/gitlab/tracking_spec.rb
@@ -180,15 +180,6 @@ RSpec.describe Gitlab::Tracking do
it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::SnowplowMicro
end
-
- it 'tracks errors' do
- expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(
- an_instance_of(ContractError),
- snowplow_category: nil, snowplow_action: 'some_action'
- )
-
- described_class.event(nil, 'some_action')
- end
end
describe '.definition' do
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 8f505606e04..05f7af7606d 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -5,8 +5,10 @@ require 'spec_helper'
RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
include StubRequests
+ let(:schemes) { %w[http https] }
+
describe '#validate!' do
- subject { described_class.validate!(import_url) }
+ subject { described_class.validate!(import_url, schemes: schemes) }
shared_examples 'validates URI and hostname' do
it 'runs the url validations' do
@@ -59,7 +61,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
end
context 'when allow_object_storage is true' do
- subject { described_class.validate!(import_url, allow_object_storage: true) }
+ subject { described_class.validate!(import_url, allow_object_storage: true, schemes: schemes) }
context 'with a local domain name' do
let(:host) { 'http://review-minio-svc.svc:9000' }
@@ -218,7 +220,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
end
context 'disabled DNS rebinding protection' do
- subject { described_class.validate!(import_url, dns_rebind_protection: false) }
+ subject { described_class.validate!(import_url, dns_rebind_protection: false, schemes: schemes) }
context 'when URI is internal' do
let(:import_url) { 'http://localhost' }
@@ -278,115 +280,114 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
it 'allows imports from configured web host and port' do
import_url = "http://#{Gitlab.host_with_port}/t.git"
- expect(described_class.blocked_url?(import_url)).to be false
+ expect(described_class.blocked_url?(import_url, schemes: schemes)).to be false
end
it 'allows mirroring from configured SSH host and port' do
import_url = "ssh://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git"
- expect(described_class.blocked_url?(import_url)).to be false
+ expect(described_class.blocked_url?(import_url, schemes: schemes)).to be false
end
it 'returns true for bad localhost hostname' do
- expect(described_class.blocked_url?('https://localhost:65535/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://localhost:65535/foo/foo.git', schemes: schemes)).to be true
end
it 'returns true for bad port' do
- expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git', ports: ports)).to be true
+ expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git', ports: ports, schemes: schemes)).to be true
end
it 'returns true for bad scheme' do
expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', schemes: ['https'])).to be false
- expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', schemes: ['http'])).to be true
end
it 'returns true for bad protocol on configured web/SSH host and ports' do
web_url = "javascript://#{Gitlab.host_with_port}/t.git%0aalert(1)"
- expect(described_class.blocked_url?(web_url)).to be true
+ expect(described_class.blocked_url?(web_url, schemes: schemes)).to be true
ssh_url = "javascript://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git%0aalert(1)"
- expect(described_class.blocked_url?(ssh_url)).to be true
+ expect(described_class.blocked_url?(ssh_url, schemes: schemes)).to be true
end
it 'returns true for localhost IPs' do
- expect(described_class.blocked_url?('https://[0:0:0:0:0:0:0:0]/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://0.0.0.0/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://[::]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[0:0:0:0:0:0:0:0]/foo/foo.git', schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://0.0.0.0/foo/foo.git', schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://[::]/foo/foo.git', schemes: schemes)).to be true
end
it 'returns true for loopback IP' do
- expect(described_class.blocked_url?('https://127.0.0.2/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://127.0.0.2/foo/foo.git', schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git', schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://[::1]/foo/foo.git', schemes: schemes)).to be true
end
it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
- expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git', schemes: schemes)).to be true
end
it 'returns true for alternative version of 127.0.0.1 (017700000001)' do
- expect(described_class.blocked_url?('https://017700000001:65535/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://017700000001:65535/foo/foo.git', schemes: schemes)).to be true
end
it 'returns true for alternative version of 127.0.0.1 (0x7f.1)' do
- expect(described_class.blocked_url?('https://0x7f.1:65535/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://0x7f.1:65535/foo/foo.git', schemes: schemes)).to be true
end
it 'returns true for alternative version of 127.0.0.1 (0x7f.0.0.1)' do
- expect(described_class.blocked_url?('https://0x7f.0.0.1:65535/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://0x7f.0.0.1:65535/foo/foo.git', schemes: schemes)).to be true
end
it 'returns true for alternative version of 127.0.0.1 (0x7f000001)' do
- expect(described_class.blocked_url?('https://0x7f000001:65535/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://0x7f000001:65535/foo/foo.git', schemes: schemes)).to be true
end
it 'returns true for alternative version of 127.0.0.1 (2130706433)' do
- expect(described_class.blocked_url?('https://2130706433:65535/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://2130706433:65535/foo/foo.git', schemes: schemes)).to be true
end
it 'returns true for alternative version of 127.0.0.1 (127.000.000.001)' do
- expect(described_class.blocked_url?('https://127.000.000.001:65535/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://127.000.000.001:65535/foo/foo.git', schemes: schemes)).to be true
end
it 'returns true for alternative version of 127.0.0.1 (127.0.1)' do
- expect(described_class.blocked_url?('https://127.0.1:65535/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://127.0.1:65535/foo/foo.git', schemes: schemes)).to be true
end
context 'with ipv6 mapped address' do
it 'returns true for localhost IPs' do
- expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:0.0.0.0]/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://[::ffff:0.0.0.0]/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://[::ffff:0:0]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:0.0.0.0]/foo/foo.git', schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://[::ffff:0.0.0.0]/foo/foo.git', schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://[::ffff:0:0]/foo/foo.git', schemes: schemes)).to be true
end
it 'returns true for loopback IPs' do
- expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:127.0.0.1]/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://[::ffff:127.0.0.1]/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://[::ffff:7f00:1]/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:127.0.0.2]/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://[::ffff:127.0.0.2]/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://[::ffff:7f00:2]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:127.0.0.1]/foo/foo.git', schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://[::ffff:127.0.0.1]/foo/foo.git', schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://[::ffff:7f00:1]/foo/foo.git', schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:127.0.0.2]/foo/foo.git', schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://[::ffff:127.0.0.2]/foo/foo.git', schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://[::ffff:7f00:2]/foo/foo.git', schemes: schemes)).to be true
end
end
it 'returns true for a non-alphanumeric hostname' do
aggregate_failures do
- expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami/a')
+ expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami/a', schemes: ['ssh'])
# The leading character here is a Unicode "soft hyphen"
- expect(described_class).to be_blocked_url('ssh://­oProxyCommand=whoami/a')
+ expect(described_class).to be_blocked_url('ssh://­oProxyCommand=whoami/a', schemes: ['ssh'])
# Unicode alphanumerics are allowed
- expect(described_class).not_to be_blocked_url('ssh://ğitlab.com/a')
+ expect(described_class).not_to be_blocked_url('ssh://ğitlab.com/a', schemes: ['ssh'])
end
end
it 'returns true for invalid URL' do
- expect(described_class.blocked_url?('http://:8080')).to be true
+ expect(described_class.blocked_url?('http://:8080', schemes: schemes)).to be true
end
it 'returns false for legitimate URL' do
- expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
+ expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', schemes: schemes)).to be false
end
context 'when allow_local_network is' do
@@ -471,33 +472,33 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
end
context 'true (default)' do
- it_behaves_like 'allows local requests', { allow_localhost: true, allow_local_network: true }
+ it_behaves_like 'allows local requests', { allow_localhost: true, allow_local_network: true, schemes: %w[http https] }
end
context 'false' do
it 'blocks urls from private networks' do
local_ips.each do |ip|
stub_domain_resolv(fake_domain, ip) do
- expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_local_network: false)
+ expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_local_network: false, schemes: schemes)
end
- expect(described_class).to be_blocked_url("http://#{ip}", allow_local_network: false)
+ expect(described_class).to be_blocked_url("http://#{ip}", allow_local_network: false, schemes: schemes)
end
end
it 'blocks IPv4 link-local endpoints' do
- expect(described_class).to be_blocked_url('http://169.254.169.254', allow_local_network: false)
- expect(described_class).to be_blocked_url('http://169.254.168.100', allow_local_network: false)
+ expect(described_class).to be_blocked_url('http://169.254.169.254', allow_local_network: false, schemes: schemes)
+ expect(described_class).to be_blocked_url('http://169.254.168.100', allow_local_network: false, schemes: schemes)
end
it 'blocks IPv6 link-local endpoints' do
- expect(described_class).to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', allow_local_network: false)
- expect(described_class).to be_blocked_url('http://[::ffff:169.254.169.254]', allow_local_network: false)
- expect(described_class).to be_blocked_url('http://[::ffff:a9fe:a9fe]', allow_local_network: false)
- expect(described_class).to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', allow_local_network: false)
- expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]', allow_local_network: false)
- expect(described_class).to be_blocked_url('http://[::ffff:a9fe:a864]', allow_local_network: false)
- expect(described_class).to be_blocked_url('http://[fe80::c800:eff:fe74:8]', allow_local_network: false)
+ expect(described_class).to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', allow_local_network: false, schemes: schemes)
+ expect(described_class).to be_blocked_url('http://[::ffff:169.254.169.254]', allow_local_network: false, schemes: schemes)
+ expect(described_class).to be_blocked_url('http://[::ffff:a9fe:a9fe]', allow_local_network: false, schemes: schemes)
+ expect(described_class).to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', allow_local_network: false, schemes: schemes)
+ expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]', allow_local_network: false, schemes: schemes)
+ expect(described_class).to be_blocked_url('http://[::ffff:a9fe:a864]', allow_local_network: false, schemes: schemes)
+ expect(described_class).to be_blocked_url('http://[fe80::c800:eff:fe74:8]', allow_local_network: false, schemes: schemes)
end
it 'blocks limited broadcast address 255.255.255.255 and variants' do
@@ -507,7 +508,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
limited_broadcast_address_variants.each do |variant|
- expect(described_class).to be_blocked_url("https://#{variant}", allow_local_network: false), "Expected #{variant} to be blocked"
+ expect(described_class).to be_blocked_url("https://#{variant}", allow_local_network: false, schemes: schemes), "Expected #{variant} to be blocked"
end
end
@@ -515,7 +516,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:url_blocker_attributes) do
{
allow_localhost: false,
- allow_local_network: false
+ allow_local_network: false,
+ schemes: schemes
}
end
@@ -545,7 +547,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
]
end
- it_behaves_like 'allows local requests', { allow_localhost: false, allow_local_network: false }
+ it_behaves_like 'allows local requests', { allow_localhost: false, allow_local_network: false, schemes: %w[http https] }
it 'allows IP when dns_rebind_protection is disabled' do
url = "http://example.com"
@@ -622,7 +624,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
end
it do
- expect(described_class).not_to be_blocked_url(url, dns_rebind_protection: dns_rebind_value)
+ expect(described_class).not_to be_blocked_url(url, dns_rebind_protection: dns_rebind_value, schemes: schemes)
end
end
@@ -676,26 +678,26 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
context 'when enforce_user is' do
context 'false (default)' do
it 'does not block urls with a non-alphanumeric username' do
- expect(described_class).not_to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a')
+ expect(described_class).not_to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a', schemes: ['ssh'])
# The leading character here is a Unicode "soft hyphen"
- expect(described_class).not_to be_blocked_url('ssh://­oProxyCommand=whoami@example.com/a')
+ expect(described_class).not_to be_blocked_url('ssh://­oProxyCommand=whoami@example.com/a', schemes: ['ssh'])
# Unicode alphanumerics are allowed
- expect(described_class).not_to be_blocked_url('ssh://ğitlab@example.com/a')
+ expect(described_class).not_to be_blocked_url('ssh://ğitlab@example.com/a', schemes: ['ssh'])
end
end
context 'true' do
it 'blocks urls with a non-alphanumeric username' do
aggregate_failures do
- expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a', enforce_user: true)
+ expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a', enforce_user: true, schemes: ['ssh'])
# The leading character here is a Unicode "soft hyphen"
- expect(described_class).to be_blocked_url('ssh://­oProxyCommand=whoami@example.com/a', enforce_user: true)
+ expect(described_class).to be_blocked_url('ssh://­oProxyCommand=whoami@example.com/a', enforce_user: true, schemes: ['ssh'])
# Unicode alphanumerics are allowed
- expect(described_class).not_to be_blocked_url('ssh://ğitlab@example.com/a', enforce_user: true)
+ expect(described_class).not_to be_blocked_url('ssh://ğitlab@example.com/a', enforce_user: true, schemes: ['ssh'])
end
end
end
@@ -703,35 +705,35 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
context 'when ascii_only is true' do
it 'returns true for unicode domain' do
- expect(described_class.blocked_url?('https://𝕘itⅼαƄ.com/foo/foo.bar', ascii_only: true)).to be true
+ expect(described_class.blocked_url?('https://𝕘itⅼαƄ.com/foo/foo.bar', ascii_only: true, schemes: schemes)).to be true
end
it 'returns true for unicode tld' do
- expect(described_class.blocked_url?('https://gitlab.ᴄοm/foo/foo.bar', ascii_only: true)).to be true
+ expect(described_class.blocked_url?('https://gitlab.ᴄοm/foo/foo.bar', ascii_only: true, schemes: schemes)).to be true
end
it 'returns true for unicode path' do
- expect(described_class.blocked_url?('https://gitlab.com/𝒇οο/𝒇οο.Ƅαꮁ', ascii_only: true)).to be true
+ expect(described_class.blocked_url?('https://gitlab.com/𝒇οο/𝒇οο.Ƅαꮁ', ascii_only: true, schemes: schemes)).to be true
end
it 'returns true for IDNA deviations' do
- expect(described_class.blocked_url?('https://mißile.com/foo/foo.bar', ascii_only: true)).to be true
- expect(described_class.blocked_url?('https://miςςile.com/foo/foo.bar', ascii_only: true)).to be true
- expect(described_class.blocked_url?('https://git‍lab.com/foo/foo.bar', ascii_only: true)).to be true
- expect(described_class.blocked_url?('https://git‌lab.com/foo/foo.bar', ascii_only: true)).to be true
+ expect(described_class.blocked_url?('https://mißile.com/foo/foo.bar', ascii_only: true, schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://miςςile.com/foo/foo.bar', ascii_only: true, schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://git‍lab.com/foo/foo.bar', ascii_only: true, schemes: schemes)).to be true
+ expect(described_class.blocked_url?('https://git‌lab.com/foo/foo.bar', ascii_only: true, schemes: schemes)).to be true
end
end
it 'blocks urls with invalid ip address' do
stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
- expect(described_class).to be_blocked_url('http://8.8.8.8.8')
+ expect(described_class).to be_blocked_url('http://8.8.8.8.8', schemes: schemes)
end
it 'blocks urls whose hostname cannot be resolved' do
stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
- expect(described_class).to be_blocked_url('http://foobar.x')
+ expect(described_class).to be_blocked_url('http://foobar.x', schemes: schemes)
end
context 'when gitlab is running on a non-default port' do
@@ -743,13 +745,13 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
it 'returns true for url targeting the wrong port' do
stub_domain_resolv('gitlab.local', '127.0.0.1') do
- expect(described_class).to be_blocked_url("http://gitlab.local/foo")
+ expect(described_class).to be_blocked_url("http://gitlab.local/foo", schemes: schemes)
end
end
it 'does not block url on gitlab port' do
stub_domain_resolv('gitlab.local', '127.0.0.1') do
- expect(described_class).not_to be_blocked_url("http://gitlab.local:#{gitlab_port}/foo")
+ expect(described_class).not_to be_blocked_url("http://gitlab.local:#{gitlab_port}/foo", schemes: schemes)
end
end
end
diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb
index 931340947a2..4b835d11975 100644
--- a/spec/lib/gitlab/usage/metric_definition_spec.rb
+++ b/spec/lib/gitlab/usage/metric_definition_spec.rb
@@ -233,6 +233,11 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
subject { described_class.send(:load_all!) }
+ after do
+ FileUtils.rm_rf(metric1)
+ FileUtils.rm_rf(metric2)
+ end
+
it 'has empty list when there are no definition files' do
is_expected.to be_empty
end
@@ -251,11 +256,6 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
subject
end
-
- after do
- FileUtils.rm_rf(metric1)
- FileUtils.rm_rf(metric2)
- end
end
describe 'dump_metrics_yaml' do
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
index 1f00f7bbec3..10e336e9235 100644
--- a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
@@ -12,6 +12,12 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi
describe '.calculate_count_for_aggregation' do
using RSpec::Parameterized::TableSyntax
+ before do
+ %w[event1 event2].each do |event_name|
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:known_event?).with(event_name).and_return(true)
+ end
+ end
+
context 'with valid configuration' do
where(:number_of_days, :operator, :datasource, :expected_method) do
28 | 'AND' | 'redis_hll' | :calculate_metrics_intersections
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_merge_request_authors_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_merge_request_authors_metric_spec.rb
deleted file mode 100644
index 92459e92eac..00000000000
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_merge_request_authors_metric_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountMergeRequestAuthorsMetric do
- let(:expected_value) { 1 }
- let(:start) { 30.days.ago.to_s(:db) }
- let(:finish) { 2.days.ago.to_s(:db) }
-
- let(:expected_query) do
- "SELECT COUNT(DISTINCT \"merge_requests\".\"author_id\") FROM \"merge_requests\"" \
- " WHERE \"merge_requests\".\"created_at\" BETWEEN '#{start}' AND '#{finish}'"
- end
-
- before do
- user = create(:user)
- user2 = create(:user)
-
- create(:merge_request, created_at: 1.year.ago, author: user)
- create(:merge_request, created_at: 1.week.ago, author: user2)
- create(:merge_request, created_at: 1.week.ago, author: user2)
- end
-
- it_behaves_like 'a correct instrumented metric value and query', { time_frame: '28d' }
-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 f1ecc8c8ab5..8ca42a6f007 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
@@ -196,6 +196,22 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
end
end
+ context 'with 7 days time frame' do
+ subject do
+ database_metric_class.tap do |metric_class|
+ metric_class.relation { Issue }
+ metric_class.operation :count
+ end.new(time_frame: '7d')
+ end
+
+ it 'calculates a correct result' do
+ create(:issue, created_at: 10.days.ago)
+ create(:issue, created_at: 5.days.ago)
+
+ expect(subject.value).to eq(1)
+ end
+ end
+
context 'with additional parameters passed via options' do
subject do
database_metric_class.tap do |metric_class|
diff --git a/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb b/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb
index 24107727a8e..9dba64ff59f 100644
--- a/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::Usage::Metrics::NameSuggestion do
let(:operation) { :count }
let(:relation) { Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot) }
let(:column) { nil }
- let(:name_suggestion) { /count_<adjective describing\: '\(issues\.author_id != \d+\)'>_issues_<with>_alert_management_alerts/ }
+ let(:name_suggestion) { /count_<adjective describing: '\(issues\.author_id != \d+\)'>_issues_<with>_alert_management_alerts/ }
end
end
end
@@ -55,7 +55,7 @@ RSpec.describe Gitlab::Usage::Metrics::NameSuggestion do
let(:operation) { :distinct_count }
let(:relation) { ::Clusters::Cluster.aws_installed.enabled.where(created_at: 30.days.ago..2.days.ago ) }
let(:column) { :user_id }
- let(:constraints) { /<adjective describing\: '\(clusters.provider_type = \d+ AND \(cluster_providers_aws\.status IN \(\d+\)\) AND clusters\.enabled = TRUE\)'>/ }
+ let(:constraints) { /<adjective describing: '\(clusters.provider_type = \d+ AND \(cluster_providers_aws\.status IN \(\d+\)\) AND clusters\.enabled = TRUE\)'>/ }
let(:name_suggestion) { /count_distinct_user_id_from_#{constraints}_clusters_<with>_#{constraints}_cluster_providers_aws/ }
end
end
@@ -66,7 +66,7 @@ RSpec.describe Gitlab::Usage::Metrics::NameSuggestion do
let(:operation) { :sum }
let(:relation) { JiraImportState.finished }
let(:column) { :imported_issues_count }
- let(:name_suggestion) { /sum_imported_issues_count_from_<adjective describing\: '\(jira_imports\.status = \d+\)'>_jira_imports/ }
+ let(:name_suggestion) { /sum_imported_issues_count_from_<adjective describing: '\(jira_imports\.status = \d+\)'>_jira_imports/ }
end
end
diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
index 7e8b15d23db..83a4ea8e948 100644
--- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
it_behaves_like 'name suggestion' do
# corresponding metric is collected with count(Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot), start: issue_minimum_id, finish: issue_maximum_id)
let(:key_path) { 'counts.issues_created_manually_from_alerts' }
- let(:name_suggestion) { /count_<adjective describing\: '\(issues\.author_id != \d+\)'>_issues_<with>_alert_management_alerts/ }
+ let(:name_suggestion) { /count_<adjective describing: '\(issues\.author_id != \d+\)'>_issues_<with>_alert_management_alerts/ }
end
end
end
@@ -54,7 +54,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
it_behaves_like 'name suggestion' do
# corresponding metric is collected with distinct_count(::Clusters::Cluster.aws_installed.enabled.where(time_period), :user_id)
let(:key_path) { 'usage_activity_by_stage_monthly.configure.clusters_platforms_eks' }
- let(:constraints) { /<adjective describing\: '\(clusters.provider_type = \d+ AND \(cluster_providers_aws\.status IN \(\d+\)\) AND clusters\.enabled = TRUE\)'>/ }
+ let(:constraints) { /<adjective describing: '\(clusters.provider_type = \d+ AND \(cluster_providers_aws\.status IN \(\d+\)\) AND clusters\.enabled = TRUE\)'>/ }
let(:name_suggestion) { /count_distinct_user_id_from_#{constraints}_clusters_<with>_#{constraints}_cluster_providers_aws/ }
end
end
@@ -63,7 +63,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
it_behaves_like 'name suggestion' do
# corresponding metric is collected with sum(JiraImportState.finished, :imported_issues_count)
let(:key_path) { 'counts.jira_imports_total_imported_issues_count' }
- let(:name_suggestion) { /sum_imported_issues_count_from_<adjective describing\: '\(jira_imports\.status = \d+\)'>_jira_imports/ }
+ let(:name_suggestion) { /sum_imported_issues_count_from_<adjective describing: '\(jira_imports\.status = \d+\)'>_jira_imports/ }
end
end
@@ -71,7 +71,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
it_behaves_like 'name suggestion' do
# corresponding metric is collected with add(data[:personal_snippets], data[:project_snippets])
let(:key_path) { 'counts.snippets' }
- let(:name_suggestion) { /add_count_<adjective describing\: '\(snippets\.type = 'PersonalSnippet'\)'>_snippets_and_count_<adjective describing\: '\(snippets\.type = 'ProjectSnippet'\)'>_snippets/ }
+ let(:name_suggestion) { /add_count_<adjective describing: '\(snippets\.type = 'PersonalSnippet'\)'>_snippets_and_count_<adjective describing: '\(snippets\.type = 'ProjectSnippet'\)'>_snippets/ }
end
end
diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb
index fb3bd564e34..3e72d118ac6 100644
--- a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb
@@ -4,7 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::RelationParsers::Joins do
describe '#accept' do
- let(:collector) { Arel::Collectors::SubstituteBinds.new(ActiveRecord::Base.connection, Arel::Collectors::SQLString.new) }
+ let(:collector) do
+ Arel::Collectors::SubstituteBinds.new(ApplicationRecord.connection, Arel::Collectors::SQLString.new)
+ end
context 'with join added via string' do
it 'collects join parts' do
@@ -33,7 +35,10 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::RelationParsers::Joins
result = described_class.new(ApplicationRecord.connection).accept(arel)
- expect(result).to match_array [{ source: "joins", constraints: "records.id = joins.records_id" }, { source: "second_level_joins", constraints: "joins.id = second_level_joins.joins_id" }]
+ expect(result).to match_array [
+ { source: "joins", constraints: "records.id = joins.records_id" },
+ { source: "second_level_joins", constraints: "joins.id = second_level_joins.joins_id" }
+ ]
end
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb b/spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb
index e122d9a3026..63a1da490ed 100644
--- a/spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Code review events' do
definition.attributes.dig(:options, :events)
end.uniq
- exceptions = %w[i_code_review_mr_diffs i_code_review_mr_with_invalid_approvers i_code_review_mr_single_file_diffs i_code_review_total_suggestions_applied i_code_review_total_suggestions_added i_code_review_create_note_in_ipynb_diff i_code_review_create_note_in_ipynb_diff_mr i_code_review_create_note_in_ipynb_diff_commit]
+ exceptions = %w[i_code_review_create_mr i_code_review_mr_diffs i_code_review_mr_with_invalid_approvers i_code_review_mr_single_file_diffs i_code_review_total_suggestions_applied i_code_review_total_suggestions_added i_code_review_create_note_in_ipynb_diff i_code_review_create_note_in_ipynb_diff_mr i_code_review_create_note_in_ipynb_diff_commit]
code_review_aggregated_events += exceptions
expect(code_review_events - code_review_aggregated_events).to be_empty
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 0bea06f602f..1d980c48c72 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -23,70 +23,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
described_class.clear_memoization(:known_events)
end
- context 'migration to instrumentation classes data collection' do
- let_it_be(:instrumented_events) do
- instrumentation_classes = %w[AggregatedMetric RedisHLLMetric]
- ::Gitlab::Usage::MetricDefinition.all.map do |definition|
- next unless definition.available?
- next unless instrumentation_classes.include?(definition.attributes[:instrumentation_class])
-
- definition.attributes.dig(:options, :events)&.sort
- end.compact.to_set
- end
-
- def not_instrumented_events(category)
- described_class
- .events_for_category(category)
- .sort
- .reject do |event|
- instrumented_events.include?([event])
- end
- end
-
- def not_instrumented_aggregate(category)
- events = described_class.events_for_category(category).sort
-
- return unless described_class::CATEGORIES_FOR_TOTALS.include?(category)
- return unless described_class.send(:eligible_for_totals?, events)
- return if instrumented_events.include?(events)
-
- events
- end
-
- describe 'Gitlab::UsageDataCounters::HLLRedisCounter::CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS' do
- it 'includes only fully migrated categories' do
- wrong_skipped_events = described_class::CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS.map do |category|
- next if not_instrumented_events(category).empty? && not_instrumented_aggregate(category).nil?
-
- [category, [not_instrumented_events(category), not_instrumented_aggregate(category)].compact]
- end.compact.to_h
-
- expect(wrong_skipped_events).to be_empty
- end
-
- context 'with not instrumented category' do
- let(:instrumented_events) { [] }
-
- it 'can detect not migrated category' do
- wrong_skipped_events = described_class::CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS.map do |category|
- next if not_instrumented_events(category).empty? && not_instrumented_aggregate(category).nil?
-
- [category, [not_instrumented_events(category), not_instrumented_aggregate(category)].compact]
- end.compact.to_h
-
- expect(wrong_skipped_events).not_to be_empty
- end
- end
- end
-
- describe '.unique_events_data' do
- it 'does not include instrumented categories' do
- expect(described_class.unique_events_data.keys)
- .not_to include(*described_class.categories_collected_from_metrics_definitions)
- end
- end
- end
-
describe '.categories' do
it 'gets CE unique category names' do
expect(described_class.categories).to include(
@@ -138,14 +74,14 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
File.open(ce_temp_file.path, "w+b") { |f| f.write [ce_event].to_yaml }
end
- it 'returns ce events' do
- expect(described_class.known_events).to include(ce_event)
- end
-
after do
ce_temp_file.unlink
FileUtils.remove_entry(ce_temp_dir) if Dir.exist?(ce_temp_dir)
end
+
+ it 'returns ce events' do
+ expect(described_class.known_events).to include(ce_event)
+ end
end
describe 'known_events' do
@@ -273,6 +209,22 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
expect { described_class.track_event('unknown', values: entity1, time: Date.current) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
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
+
+ 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)
@@ -342,7 +294,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
context 'with valid contex' do
it 'increments context event counter' do
expect(Gitlab::Redis::HLL).to receive(:add) do |kwargs|
- expect(kwargs[:key]).to match(/^#{default_context}\_.*/)
+ expect(kwargs[:key]).to match(/^#{default_context}_.*/)
end
described_class.track_event_in_context(context_event, values: entity1, context: default_context)
@@ -544,53 +496,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
end
- describe 'unique_events_data' do
- let(:known_events) do
- [
- { name: 'event1_slot', redis_slot: "slot", category: 'category1', aggregation: "weekly" },
- { name: 'event2_slot', redis_slot: "slot", category: 'category1', aggregation: "weekly" },
- { name: 'event3', category: 'category2', aggregation: "weekly" },
- { name: 'event4', category: 'category2', aggregation: "weekly" }
- ].map(&:with_indifferent_access)
- end
-
- before do
- allow(described_class).to receive(:known_events).and_return(known_events)
- allow(described_class).to receive(:categories).and_return(%w(category1 category2))
-
- stub_const('Gitlab::UsageDataCounters::HLLRedisCounter::CATEGORIES_FOR_TOTALS', %w(category1 category2))
-
- described_class.track_event('event1_slot', values: entity1, time: 2.days.ago)
- described_class.track_event('event2_slot', values: entity2, time: 2.days.ago)
- described_class.track_event('event2_slot', values: entity3, time: 2.weeks.ago)
-
- # events in different slots
- described_class.track_event('event3', values: entity2, time: 2.days.ago)
- described_class.track_event('event4', values: entity2, time: 2.days.ago)
- end
-
- it 'returns the number of unique events for all known events' do
- results = {
- "category1" => {
- "event1_slot_weekly" => 1,
- "event1_slot_monthly" => 1,
- "event2_slot_weekly" => 1,
- "event2_slot_monthly" => 2,
- "category1_total_unique_counts_weekly" => 2,
- "category1_total_unique_counts_monthly" => 3
- },
- "category2" => {
- "event3_weekly" => 1,
- "event3_monthly" => 1,
- "event4_weekly" => 1,
- "event4_monthly" => 1
- }
- }
-
- expect(subject.unique_events_data).to eq(results)
- end
- end
-
describe '.calculate_events_union' do
let(:time_range) { { start_date: 7.days.ago, end_date: DateTime.current } }
let(:known_events) do
diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb
index 74e63d219bd..9a1ffd8d01d 100644
--- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb
@@ -50,11 +50,29 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
end
describe '.track_create_mr_action' do
- subject { described_class.track_create_mr_action(user: user) }
+ subject { described_class.track_create_mr_action(user: user, merge_request: merge_request) }
+
+ let(:merge_request) { create(:merge_request) }
+ let(:target_project) { merge_request.target_project }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_USER_CREATE_ACTION }
+ end
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_CREATE_ACTION }
end
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:action) { :create }
+ let(:category) { described_class.name }
+ let(:project) { target_project }
+ let(:namespace) { project.namespace.reload }
+ let(:user) { project.creator }
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:label) { 'redis_hll_counters.code_review.i_code_review_create_mr_monthly' }
+ let(:property) { described_class::MR_CREATE_ACTION }
+ end
end
describe '.track_close_mr_action' do
@@ -94,30 +112,15 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
let(:action) { described_class::MR_APPROVE_ACTION }
end
- it 'records correct payload with Snowplow event', :snowplow do
- stub_feature_flags(route_hll_to_snowplow_phase2: true)
-
- subject
-
- expect_snowplow_event(
- category: 'merge_requests',
- action: 'i_code_review_user_approve_mr',
- namespace: target_project.namespace,
- user: user,
- project: target_project
- )
- end
-
- context 'when FF is disabled' do
- before do
- stub_feature_flags(route_hll_to_snowplow_phase2: false)
- end
-
- it 'doesnt emit snowplow events', :snowplow do
- subject
-
- expect_no_snowplow_event
- end
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:action) { :approve }
+ let(:category) { described_class.name }
+ let(:project) { target_project }
+ let(:namespace) { project.namespace.reload }
+ let(:user) { project.creator }
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:label) { 'redis_hll_counters.code_review.i_code_review_user_approve_mr_monthly' }
+ let(:property) { described_class::MR_APPROVE_ACTION }
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index d8f50fa27bb..214331e15e8 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -598,35 +598,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
external_diffs: { enabled: false },
lfs: { enabled: true, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } },
uploads: { enabled: nil, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } },
- packages: { enabled: true, object_store: { enabled: false, direct_upload: false, background_upload: true, provider: "AWS" } } }
+ packages: { enabled: true, object_store: { enabled: false, direct_upload: false, background_upload: false, provider: "AWS" } } }
)
end
- context 'with existing container expiration policies' do
- let_it_be(:disabled) { create(:container_expiration_policy, enabled: false) }
- let_it_be(:enabled) { create(:container_expiration_policy, enabled: true) }
-
- ::ContainerExpirationPolicy.older_than_options.keys.each do |value|
- let_it_be("container_expiration_policy_with_older_than_set_to_#{value}") { create(:container_expiration_policy, older_than: value) }
- end
-
- let_it_be('container_expiration_policy_with_older_than_set_to_null') { create(:container_expiration_policy, older_than: nil) }
-
- let(:inactive_policies) { ::ContainerExpirationPolicy.where(enabled: false) }
- let(:active_policies) { ::ContainerExpirationPolicy.active }
-
- subject { described_class.data[:counts] }
-
- it 'gathers usage data' do
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_unset]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_7d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_14d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_30d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_60d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_90d]).to eq 2
- end
- end
-
context 'when queries time out' do
let(:metric_method) { :count }
@@ -860,7 +835,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
'direct_upload' => true,
'connection' =>
{ 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
- 'background_upload' => false,
'proxy_download' => false } })
expect(subject).to eq(
@@ -1135,36 +1109,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
- describe 'redis_hll_counters' do
- subject { described_class.redis_hll_counters }
-
- let(:migrated_categories) do
- ::Gitlab::UsageDataCounters::HLLRedisCounter.categories_collected_from_metrics_definitions
- end
-
- let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories - migrated_categories }
- let(:ignored_metrics) { ["i_package_composer_deploy_token_weekly"] }
-
- it 'has all known_events' do
- expect(subject).to have_key(:redis_hll_counters)
-
- expect(subject[:redis_hll_counters].keys).to match_array(categories)
-
- categories.each do |category|
- keys = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(category)
-
- metrics = keys.map { |key| "#{key}_weekly" } + keys.map { |key| "#{key}_monthly" }
- metrics -= ignored_metrics
-
- if ::Gitlab::UsageDataCounters::HLLRedisCounter::CATEGORIES_FOR_TOTALS.include?(category)
- metrics.append("#{category}_total_unique_counts_weekly", "#{category}_total_unique_counts_monthly")
- end
-
- expect(subject[:redis_hll_counters][category].keys).to match_array(metrics)
- end
- end
- end
-
describe '.service_desk_counts' do
subject { described_class.send(:service_desk_counts) }
diff --git a/spec/lib/gitlab/utils/delegator_override/validator_spec.rb b/spec/lib/gitlab/utils/delegator_override/validator_spec.rb
index a58bc65c708..4fcf01ea256 100644
--- a/spec/lib/gitlab/utils/delegator_override/validator_spec.rb
+++ b/spec/lib/gitlab/utils/delegator_override/validator_spec.rb
@@ -7,8 +7,7 @@ RSpec.describe Gitlab::Utils::DelegatorOverride::Validator do
Class.new(::SimpleDelegator) do
extend(::Gitlab::Utils::DelegatorOverride)
- def foo
- end
+ def foo; end
end.prepend(ee_delegator_extension)
end
@@ -16,18 +15,15 @@ RSpec.describe Gitlab::Utils::DelegatorOverride::Validator do
Module.new do
extend(::Gitlab::Utils::DelegatorOverride)
- def bar
- end
+ def bar; end
end
end
let(:target_class) do
Class.new do
- def foo
- end
+ def foo; end
- def bar
- end
+ def bar; end
end
end
diff --git a/spec/lib/gitlab/utils/delegator_override_spec.rb b/spec/lib/gitlab/utils/delegator_override_spec.rb
index 2dafa75e344..b566b7a2cad 100644
--- a/spec/lib/gitlab/utils/delegator_override_spec.rb
+++ b/spec/lib/gitlab/utils/delegator_override_spec.rb
@@ -7,25 +7,21 @@ RSpec.describe Gitlab::Utils::DelegatorOverride do
Class.new(::SimpleDelegator) do
extend(::Gitlab::Utils::DelegatorOverride)
- def foo
- end
+ def foo; end
end
end
let(:target_class) do
Class.new do
- def foo
- end
+ def foo; end
- def bar
- end
+ def bar; end
end
end
let(:dummy_module) do
Module.new do
- def foobar
- end
+ def foobar; end
end
end
diff --git a/spec/lib/gitlab/utils/override_spec.rb b/spec/lib/gitlab/utils/override_spec.rb
index a5e53c1dfc1..63f7b1623d8 100644
--- a/spec/lib/gitlab/utils/override_spec.rb
+++ b/spec/lib/gitlab/utils/override_spec.rb
@@ -35,8 +35,7 @@ RSpec.describe Gitlab::Utils::Override do
override :good
if bad_arity
- def good(num)
- end
+ def good(num); end
elsif negative_arity
def good(*args)
super.succ
diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb
index 236b6d29ba7..287858579d6 100644
--- a/spec/lib/gitlab/utils/strong_memoize_spec.rb
+++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
end
def method_name
- strong_memoize(:method_name) do
+ strong_memoize(:method_name) do # rubocop: disable Gitlab/StrongMemoizeAttr
trace << value
value
end
@@ -59,22 +59,19 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
protected
- def private_method
- end
+ def private_method; end
private :private_method
strong_memoize_attr :private_method
public
- def protected_method
- end
+ def protected_method; end
protected :protected_method
strong_memoize_attr :protected_method
private
- def public_method
- end
+ def public_method; end
public :public_method
strong_memoize_attr :public_method
end
@@ -219,6 +216,10 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
it 'calls the existing .method_added' do
expect(klass.method_added_list).to include(:method_name_attr)
end
+
+ it 'retains method arity' do
+ expect(klass.instance_method(member_name).arity).to eq(0)
+ end
end
context "memoized before method definition with different member name and value #{value}" do
@@ -280,5 +281,22 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
expect { subject }.to raise_error(NameError, %r{undefined method `nonexistent_method' for class})
end
end
+
+ context 'when memoized method has parameters' do
+ it 'raises an error' do
+ expected_message = /Using `strong_memoize_attr` on methods with parameters is not supported/
+
+ expect do
+ strong_memoize_class = described_class
+
+ Class.new do
+ include strong_memoize_class
+
+ def method_with_parameters(params); end
+ strong_memoize_attr :method_with_parameters
+ end
+ end.to raise_error(RuntimeError, expected_message)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/work_items/work_item_hierarchy_spec.rb b/spec/lib/gitlab/work_items/work_item_hierarchy_spec.rb
new file mode 100644
index 00000000000..b2f298a7d05
--- /dev/null
+++ b/spec/lib/gitlab/work_items/work_item_hierarchy_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::WorkItems::WorkItemHierarchy, feature_category: :portfolio_management do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:type1) { create(:work_item_type, namespace: project.namespace) }
+ let_it_be(:type2) { create(:work_item_type, namespace: project.namespace) }
+ let_it_be(:hierarchy_restriction1) { create(:hierarchy_restriction, parent_type: type1, child_type: type2) }
+ let_it_be(:hierarchy_restriction2) { create(:hierarchy_restriction, parent_type: type2, child_type: type2) }
+ let_it_be(:hierarchy_restriction3) { create(:hierarchy_restriction, parent_type: type2, child_type: type1) }
+ let_it_be(:item1) { create(:work_item, work_item_type: type1, project: project) }
+ let_it_be(:item2) { create(:work_item, work_item_type: type2, project: project) }
+ let_it_be(:item3) { create(:work_item, work_item_type: type2, project: project) }
+ let_it_be(:item4) { create(:work_item, work_item_type: type1, project: project) }
+ let_it_be(:ignored1) { create(:work_item, work_item_type: type1, project: project) }
+ let_it_be(:ignored2) { create(:work_item, work_item_type: type2, project: project) }
+ let_it_be(:link1) { create(:parent_link, work_item_parent: item1, work_item: item2) }
+ let_it_be(:link2) { create(:parent_link, work_item_parent: item2, work_item: item3) }
+ let_it_be(:link3) { create(:parent_link, work_item_parent: item3, work_item: item4) }
+
+ let(:options) { {} }
+
+ describe '#base_and_ancestors' do
+ subject { described_class.new(::WorkItem.where(id: item3.id), options: options) }
+
+ it 'includes the base and its ancestors' do
+ relation = subject.base_and_ancestors
+
+ expect(relation).to eq([item3, item2, item1])
+ end
+
+ context 'when same_type option is used' do
+ let(:options) { { same_type: true } }
+
+ it 'includes the base and its ancestors' do
+ relation = subject.base_and_ancestors
+
+ expect(relation).to eq([item3, item2])
+ end
+ end
+
+ it 'can find ancestors upto a certain level' do
+ relation = subject.base_and_ancestors(upto: item1)
+
+ expect(relation).to eq([item3, item2])
+ end
+
+ describe 'hierarchy_order option' do
+ let(:relation) do
+ subject.base_and_ancestors(hierarchy_order: hierarchy_order)
+ end
+
+ context 'for :asc' do
+ let(:hierarchy_order) { :asc }
+
+ it 'orders by child to ancestor' do
+ expect(relation).to eq([item3, item2, item1])
+ end
+ end
+
+ context 'for :desc' do
+ let(:hierarchy_order) { :desc }
+
+ it 'orders by ancestor to child' do
+ expect(relation).to eq([item1, item2, item3])
+ end
+ end
+ end
+ end
+
+ describe '#base_and_descendants' do
+ subject { described_class.new(::WorkItem.where(id: item2.id), options: options) }
+
+ it 'includes the base and its descendants' do
+ relation = subject.base_and_descendants
+
+ expect(relation).to eq([item2, item3, item4])
+ end
+
+ context 'when same_type option is used' do
+ let(:options) { { same_type: true } }
+
+ it 'includes the base and its ancestors' do
+ relation = subject.base_and_descendants
+
+ expect(relation).to eq([item2, item3])
+ end
+ end
+
+ context 'when with_depth is true' do
+ let(:relation) do
+ subject.base_and_descendants(with_depth: true)
+ end
+
+ it 'includes depth in the results' do
+ object_depths = {
+ item2.id => 1,
+ item3.id => 2,
+ item4.id => 3
+ }
+
+ relation.each do |object|
+ expect(object.depth).to eq(object_depths[object.id])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 5c9a3cc0a24..3c7542ea5f9 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Workhorse do
let_it_be(:project) { create(:project, :repository) }
+ let(:features) { { 'gitaly-feature-enforce-requests-limits' => 'true' } }
let(:repository) { project.repository }
@@ -42,7 +43,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-archive')
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
+ 'call_metadata' => features,
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -92,7 +93,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq("git-format-patch")
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
+ 'call_metadata' => features,
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -155,7 +156,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq("git-diff")
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
+ 'call_metadata' => features,
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -208,6 +209,16 @@ RSpec.describe Gitlab::Workhorse do
describe '.git_http_ok' do
let(:user) { create(:user) }
+ let(:gitaly_params) do
+ {
+ GitalyServer: {
+ call_metadata: call_metadata,
+ address: Gitlab::GitalyClient.address('default'),
+ token: Gitlab::GitalyClient.token('default')
+ }
+ }
+ end
+
let(:repo_path) { 'ignored but not allowed to be empty in gitlab-workhorse' }
let(:action) { 'info_refs' }
let(:params) do
@@ -219,6 +230,13 @@ RSpec.describe Gitlab::Workhorse do
}
end
+ let(:call_metadata) do
+ features.merge({
+ 'user_id' => params[:GL_ID],
+ 'username' => params[:GL_USERNAME]
+ })
+ end
+
subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action) }
it { expect(subject).to include(params) }
@@ -238,100 +256,79 @@ RSpec.describe Gitlab::Workhorse do
it { expect(subject).to include(params) }
end
- context 'when Gitaly is enabled' do
- let(:gitaly_params) do
- {
- GitalyServer: {
- features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
- address: Gitlab::GitalyClient.address('default'),
- token: Gitlab::GitalyClient.token('default')
- }
- }
- end
+ it 'includes a Repository param' do
+ repo_param = {
+ storage_name: 'default',
+ relative_path: project.disk_path + '.git',
+ gl_repository: "project-#{project.id}"
+ }
- before do
- allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
- end
+ expect(subject[:Repository]).to include(repo_param)
+ end
- it 'includes a Repository param' do
- repo_param = {
- storage_name: 'default',
- relative_path: project.disk_path + '.git',
- gl_repository: "project-#{project.id}"
- }
+ context "when git_upload_pack action is passed" do
+ let(:action) { 'git_upload_pack' }
- expect(subject[:Repository]).to include(repo_param)
- end
+ it { expect(subject).to include(gitaly_params) }
- context "when git_upload_pack action is passed" do
- let(:action) { 'git_upload_pack' }
- let(:feature_flag) { :post_upload_pack }
+ context 'show_all_refs enabled' do
+ subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) }
- it 'includes Gitaly params in the returned value' do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(true)
+ it { is_expected.to include(ShowAllRefs: true) }
+ end
- expect(subject).to include(gitaly_params)
+ context 'when a feature flag is set for a single project' do
+ before do
+ stub_feature_flags(gitaly_mep_mep: project)
end
- context 'show_all_refs enabled' do
- subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) }
+ it 'sets the flag to true for that project' do
+ response = described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action)
- it { is_expected.to include(ShowAllRefs: true) }
+ expect(response.dig(:GitalyServer, :call_metadata)).to include('gitaly-feature-enforce-requests-limits' => 'true',
+ 'gitaly-feature-mep-mep' => 'true')
end
- context 'when a feature flag is set for a single project' do
- before do
- stub_feature_flags(gitaly_mep_mep: project)
- end
+ it 'sets the flag to false for other projects' do
+ other_project = create(:project, :public, :repository)
+ response = described_class.git_http_ok(other_project.repository, Gitlab::GlRepository::PROJECT, user, action)
- it 'sets the flag to true for that project' do
- response = described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action)
-
- expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true',
- 'gitaly-feature-mep-mep' => 'true')
- end
-
- it 'sets the flag to false for other projects' do
- other_project = create(:project, :public, :repository)
- response = described_class.git_http_ok(other_project.repository, Gitlab::GlRepository::PROJECT, user, action)
-
- expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true',
- 'gitaly-feature-mep-mep' => 'false')
- end
+ expect(response.dig(:GitalyServer, :call_metadata)).to include('gitaly-feature-enforce-requests-limits' => 'true',
+ 'gitaly-feature-mep-mep' => 'false')
+ end
- it 'sets the flag to false when there is no project' do
- snippet = create(:personal_snippet, :repository)
- response = described_class.git_http_ok(snippet.repository, Gitlab::GlRepository::SNIPPET, user, action)
+ it 'sets the flag to false when there is no project' do
+ snippet = create(:personal_snippet, :repository)
+ response = described_class.git_http_ok(snippet.repository, Gitlab::GlRepository::SNIPPET, user, action)
- expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true',
- 'gitaly-feature-mep-mep' => 'false')
- end
+ expect(response.dig(:GitalyServer, :call_metadata)).to include('gitaly-feature-enforce-requests-limits' => 'true',
+ 'gitaly-feature-mep-mep' => 'false')
end
end
+ end
- context "when git_receive_pack action is passed" do
- let(:action) { 'git_receive_pack' }
+ context "when git_receive_pack action is passed" do
+ let(:action) { 'git_receive_pack' }
- it { expect(subject).to include(gitaly_params) }
- end
+ it { expect(subject).to include(gitaly_params) }
+ end
- context "when info_refs action is passed" do
- let(:action) { 'info_refs' }
+ context "when info_refs action is passed" do
+ let(:action) { 'info_refs' }
- it { expect(subject).to include(gitaly_params) }
+ it { expect(subject).to include(gitaly_params) }
- context 'show_all_refs enabled' do
- subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) }
+ context 'show_all_refs enabled' do
+ subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) }
- it { is_expected.to include(ShowAllRefs: true) }
- end
+ it { is_expected.to include(ShowAllRefs: true) }
end
+ end
- context 'when action passed is not supported by Gitaly' do
- let(:action) { 'download' }
+ context 'when action passed is not supported by Gitaly' do
+ let(:action) { 'download' }
- it { expect { subject }.to raise_exception('Unsupported action: download') }
- end
+ it { expect { subject }.to raise_exception('Unsupported action: download') }
end
context 'when receive_max_input_size has been updated' do
@@ -349,6 +346,23 @@ RSpec.describe Gitlab::Workhorse do
expect(subject[:GitConfigOptions]).to be_empty
end
end
+
+ context 'when remote_ip is available in the application context' do
+ it 'includes a RemoteIP params' do
+ result = {}
+ Gitlab::ApplicationContext.with_context(remote_ip: "1.2.3.4") do
+ result = described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action)
+ end
+ expect(result[:GitalyServer][:call_metadata]['remote_ip']).to eql("1.2.3.4")
+ end
+ end
+
+ context 'when remote_ip is not available in the application context' do
+ it 'does not include RemoteIP params' do
+ result = described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action)
+ expect(result[:GitalyServer][:call_metadata]).not_to have_key('remote_ip')
+ end
+ end
end
describe '.set_key_and_notify' do
@@ -428,7 +442,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-blob')
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
+ 'call_metadata' => features,
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -508,7 +522,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-snapshot')
expect(params).to eq(
'GitalyServer' => {
- 'features' => { 'gitaly-feature-enforce-requests-limits' => 'true' },
+ 'call_metadata' => features,
'address' => Gitlab::GitalyClient.address(project.repository_storage),
'token' => Gitlab::GitalyClient.token(project.repository_storage)
},
diff --git a/spec/lib/gitlab/x509/signature_spec.rb b/spec/lib/gitlab/x509/signature_spec.rb
index 31f66232f38..eb8c0bd0aff 100644
--- a/spec/lib/gitlab/x509/signature_spec.rb
+++ b/spec/lib/gitlab/x509/signature_spec.rb
@@ -11,6 +11,17 @@ RSpec.describe Gitlab::X509::Signature do
}
end
+ it_behaves_like 'signature with type checking', :x509 do
+ subject(:signature) do
+ described_class.new(
+ X509Helpers::User1.signed_commit_signature,
+ X509Helpers::User1.signed_commit_base_data,
+ X509Helpers::User1.certificate_email,
+ X509Helpers::User1.signed_commit_time
+ )
+ end
+ end
+
shared_examples "a verified signature" do
let!(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
@@ -271,21 +282,21 @@ RSpec.describe Gitlab::X509::Signature do
end
end
- describe '#user' do
+ describe '#signed_by_user' do
subject do
described_class.new(
X509Helpers::User1.signed_tag_signature,
X509Helpers::User1.signed_tag_base_data,
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
- ).user
+ ).signed_by_user
end
context 'if email is assigned to a user' do
- let!(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
+ let!(:signed_by_user) { create(:user, email: X509Helpers::User1.certificate_email) }
it 'returns user' do
- is_expected.to eq(user)
+ is_expected.to eq(signed_by_user)
end
end
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index 0c207161927..82ab6c089da 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -106,8 +106,8 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
let(:enable_addons) { [] }
let(:addons_config) do
- enable_addons.each_with_object({}) do |addon, hash|
- hash[addon] = { disabled: false }
+ enable_addons.index_with do
+ { disabled: false }
end
end
diff --git a/spec/lib/json_web_token/hmac_token_spec.rb b/spec/lib/json_web_token/hmac_token_spec.rb
index cf7e5c54f45..016084eaf69 100644
--- a/spec/lib/json_web_token/hmac_token_spec.rb
+++ b/spec/lib/json_web_token/hmac_token_spec.rb
@@ -1,9 +1,11 @@
# frozen_string_literal: true
require 'json'
-require 'timecop'
+require 'active_support/testing/time_helpers'
RSpec.describe JSONWebToken::HMACToken do
+ include ActiveSupport::Testing::TimeHelpers
+
let(:secret) { 'shh secret squirrel' }
shared_examples 'a valid, non-expired token' do
@@ -54,13 +56,13 @@ RSpec.describe JSONWebToken::HMACToken do
end
context 'that is expired' do
- # Needs the ! so Timecop.freeze() is effective
+ # Needs the ! so freeze_time() is effective
let!(:encoded_token) { described_class.new(secret).encoded }
it "raises exception saying 'Signature has expired'" do
# Needs to be 120 seconds, because the default expiry is 60 seconds
# with an additional 60 second leeway.
- Timecop.freeze(Time.now + 120) do
+ travel_to(Time.now + 120) do
expect { decoded_token }.to raise_error(JWT::ExpiredSignature, 'Signature has expired')
end
end
@@ -77,19 +79,19 @@ RSpec.describe JSONWebToken::HMACToken do
context 'that has expired' do
let(:expire_time) { 0 }
+ around do |example|
+ travel_to(Time.now + 1) { example.run }
+ end
+
context 'with the default leeway' do
- Timecop.freeze(Time.now + 1) do
- it_behaves_like 'a valid, non-expired token'
- end
+ it_behaves_like 'a valid, non-expired token'
end
context 'with a leeway of 0 seconds' do
let(:leeway) { 0 }
it "raises exception saying 'Signature has expired'" do
- Timecop.freeze(Time.now + 1) do
- expect { decoded_token }.to raise_error(JWT::ExpiredSignature, 'Signature has expired')
- end
+ expect { decoded_token }.to raise_error(JWT::ExpiredSignature, 'Signature has expired')
end
end
end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index d208ef93224..87e2e341777 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -14,15 +14,15 @@ RSpec.describe Mattermost::Session, type: :request do
subject { described_class.new(user) }
# Needed for doorkeeper to function
+ before do
+ subject.base_uri = mattermost_url
+ end
+
it { is_expected.to respond_to(:current_resource_owner) }
it { is_expected.to respond_to(:request) }
it { is_expected.to respond_to(:authorization) }
it { is_expected.to respond_to(:strategy) }
- before do
- subject.base_uri = mattermost_url
- end
-
describe '#with session' do
let(:location) { 'http://location.tld' }
let(:cookie_header) { 'MMOAUTH=taskik8az7rq8k6rkpuas7htia; Path=/;' }
diff --git a/spec/lib/pager_duty/webhook_payload_parser_spec.rb b/spec/lib/pager_duty/webhook_payload_parser_spec.rb
index 647f19e3d3a..1606d03c746 100644
--- a/spec/lib/pager_duty/webhook_payload_parser_spec.rb
+++ b/spec/lib/pager_duty/webhook_payload_parser_spec.rb
@@ -10,23 +10,27 @@ RSpec.describe PagerDuty::WebhookPayloadParser do
let(:triggered_event) do
{
- 'event' => 'incident.trigger',
+ 'event' => 'incident.triggered',
'incident' => {
- 'url' => 'https://webdemo.pagerduty.com/incidents/PRORDTY',
- 'incident_number' => 33,
- 'title' => 'My new incident',
+ 'url' => 'https://gitlab-1.pagerduty.com/incidents/Q1XZUF87W1HB5A',
+ 'incident_number' => 2,
+ 'title' => '[FILTERED]',
'status' => 'triggered',
- 'created_at' => '2017-09-26T15:14:36Z',
+ 'created_at' => '2022-11-30T08:46:19Z',
'urgency' => 'high',
- 'incident_key' => nil,
- 'assignees' => [{
- 'summary' => 'Laura Haley',
- 'url' => 'https://webdemo.pagerduty.com/users/P553OPV'
- }],
- 'impacted_services' => [{
- 'summary' => 'Production XDB Cluster',
- 'url' => 'https://webdemo.pagerduty.com/services/PN49J75'
- }]
+ 'incident_key' => '[FILTERED]',
+ 'assignees' =>
+ [
+ {
+ 'summary' => 'Rajendra Kadam',
+ 'url' => 'https://gitlab-1.pagerduty.com/users/PIN0B5C'
+ }
+ ],
+ 'impacted_service' =>
+ {
+ 'summary' => 'Test service',
+ 'url' => 'https://gitlab-1.pagerduty.com/services/PK6IKMT'
+ }
}
}
end
@@ -37,74 +41,50 @@ RSpec.describe PagerDuty::WebhookPayloadParser do
let(:payload) { Gitlab::Json.parse(fixture_file) }
it 'returns parsed payload' do
- is_expected.to eq([triggered_event])
+ is_expected.to eq(triggered_event)
end
context 'when assignments summary and html_url are blank' do
before do
- payload['messages'].each do |m|
- m['incident']['assignments'] = [{ 'assignee' => { 'summary' => '', 'html_url' => '' } }]
- end
+ payload['event']['data']['assignees'] = [{ 'summary' => '', 'html_url' => '' }]
end
it 'returns parsed payload with blank assignees' do
- assignees = parse.map { |events| events['incident'].slice('assignees') }
+ assignees = parse['incident'].slice('assignees')
- expect(assignees).to eq([{ 'assignees' => [] }])
+ expect(assignees).to eq({ 'assignees' => [] })
end
end
context 'when impacted_services summary and html_url are blank' do
before do
- payload['messages'].each do |m|
- m['incident']['impacted_services'] = [{ 'summary' => '', 'html_url' => '' }]
- end
+ payload['event']['data']['service'] = { 'summary' => '', 'html_url' => '' }
end
- it 'returns parsed payload with blank assignees' do
- assignees = parse.map { |events| events['incident'].slice('impacted_services') }
+ it 'returns parsed payload with blank impacted service' do
+ assignees = parse['incident'].slice('impacted_service')
- expect(assignees).to eq([{ 'impacted_services' => [] }])
+ expect(assignees).to eq({ 'impacted_service' => {} })
end
end
end
context 'when payload schema is invalid' do
- let(:payload) { { 'messages' => [{ 'event' => 'incident.trigger' }] } }
+ let(:payload) { { 'event' => 'incident.triggered' } }
- it 'returns payload with blank incidents' do
- is_expected.to eq([])
+ it 'returns payload with blank incident' do
+ is_expected.to eq({})
end
end
- context 'when payload consists of two messages' do
- context 'when one of the messages has no incident data' do
- let(:payload) do
- valid_payload = Gitlab::Json.parse(fixture_file)
- event = { 'event' => 'incident.trigger' }
- valid_payload['messages'] = valid_payload['messages'].append(event)
- valid_payload
- end
-
- it 'returns parsed payload with valid events only' do
- is_expected.to eq([triggered_event])
- end
+ context 'when event is unknown' do
+ let(:payload) do
+ valid_payload = Gitlab::Json.parse(fixture_file)
+ valid_payload['event'] = 'incident.unknown'
end
- context 'when one of the messages has unknown event' do
- let(:payload) do
- valid_payload = Gitlab::Json.parse(fixture_file)
- event = { 'event' => 'incident.unknown', 'incident' => valid_payload['messages'].first['incident'] }
- valid_payload['messages'] = valid_payload['messages'].append(event)
- valid_payload
- end
-
- it 'returns parsed payload' do
- unknown_event = triggered_event.dup
- unknown_event['event'] = 'incident.unknown'
-
- is_expected.to contain_exactly(triggered_event, unknown_event)
- end
+ it 'returns empty payload' do
+ is_expected.to be_empty
end
end
end
diff --git a/spec/lib/peek/views/active_record_spec.rb b/spec/lib/peek/views/active_record_spec.rb
index 7bc15f40065..fc768bdcb82 100644
--- a/spec/lib/peek/views/active_record_spec.rb
+++ b/spec/lib/peek/views/active_record_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
end
it 'includes db role data and db_config_name name' do
- Timecop.freeze(2021, 2, 23, 10, 0) do
+ travel_to(Time.utc(2021, 2, 23, 10, 0)) do
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1)
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 2.seconds, '2', event_2)
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 3.seconds, '3', event_3)
diff --git a/spec/lib/sbom/package_url/argument_validator_spec.rb b/spec/lib/sbom/package_url/argument_validator_spec.rb
index 246da1c0bda..56dc1d54ba9 100644
--- a/spec/lib/sbom/package_url/argument_validator_spec.rb
+++ b/spec/lib/sbom/package_url/argument_validator_spec.rb
@@ -5,7 +5,7 @@ require 'rspec-parameterized'
require_relative '../../../support/shared_contexts/lib/sbom/package_url_shared_contexts'
-RSpec.describe Sbom::PackageUrl::ArgumentValidator do
+RSpec.describe Sbom::PackageUrl::ArgumentValidator, feature_category: :dependency_management do
let(:mock_package_url) { Struct.new(:type, :namespace, :name, :version, :qualifiers, keyword_init: true) }
let(:package) do
mock_package_url.new(
diff --git a/spec/lib/sbom/package_url/decoder_spec.rb b/spec/lib/sbom/package_url/decoder_spec.rb
index 5b480475b7c..3c092b35ea2 100644
--- a/spec/lib/sbom/package_url/decoder_spec.rb
+++ b/spec/lib/sbom/package_url/decoder_spec.rb
@@ -5,7 +5,7 @@ require 'rspec-parameterized'
require_relative '../../../support/shared_contexts/lib/sbom/package_url_shared_contexts'
-RSpec.describe Sbom::PackageUrl::Decoder do
+RSpec.describe Sbom::PackageUrl::Decoder, feature_category: :dependency_management do
describe '#decode' do
subject(:decode) { described_class.new(purl).decode! }
diff --git a/spec/lib/sbom/package_url/encoder_spec.rb b/spec/lib/sbom/package_url/encoder_spec.rb
index bdbd61636b5..a0b51007008 100644
--- a/spec/lib/sbom/package_url/encoder_spec.rb
+++ b/spec/lib/sbom/package_url/encoder_spec.rb
@@ -5,7 +5,7 @@ require 'rspec-parameterized'
require_relative '../../../support/shared_contexts/lib/sbom/package_url_shared_contexts'
-RSpec.describe Sbom::PackageUrl::Encoder do
+RSpec.describe Sbom::PackageUrl::Encoder, feature_category: :dependency_management do
describe '#encode' do
let(:package) do
::Sbom::PackageUrl.new(
diff --git a/spec/lib/sbom/package_url/normalizer_spec.rb b/spec/lib/sbom/package_url/normalizer_spec.rb
index bbc2bd3ca13..3ad548a5c84 100644
--- a/spec/lib/sbom/package_url/normalizer_spec.rb
+++ b/spec/lib/sbom/package_url/normalizer_spec.rb
@@ -5,7 +5,7 @@ require 'rspec-parameterized'
require_relative '../../../support/shared_contexts/lib/sbom/package_url_shared_contexts'
-RSpec.describe Sbom::PackageUrl::Normalizer do
+RSpec.describe Sbom::PackageUrl::Normalizer, feature_category: :dependency_management do
shared_examples 'name normalization' do
context 'with bitbucket url' do
let(:type) { 'bitbucket' }
diff --git a/spec/lib/sbom/package_url_spec.rb b/spec/lib/sbom/package_url_spec.rb
index 6760b0a68e5..92490b184df 100644
--- a/spec/lib/sbom/package_url_spec.rb
+++ b/spec/lib/sbom/package_url_spec.rb
@@ -29,7 +29,7 @@ require 'rspec-parameterized'
require_relative '../../support/helpers/next_instance_of'
require_relative '../../support/shared_contexts/lib/sbom/package_url_shared_contexts'
-RSpec.describe Sbom::PackageUrl do
+RSpec.describe Sbom::PackageUrl, feature_category: :dependency_management do
include NextInstanceOf
describe '#initialize' do
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 38066e41c53..5b1db66beb0 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
@@ -33,7 +33,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
RANDOM: make sure this persists
include:
- template: existing.yml
- - template: Security/Container-Scanning.gitlab-ci.yml
+ - template: Jobs/Container-Scanning.gitlab-ci.yml
CI_YML
end
@@ -85,7 +85,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
variables:
RANDOM: make sure this persists
include:
- - template: Security/Container-Scanning.gitlab-ci.yml
+ - template: Jobs/Container-Scanning.gitlab-ci.yml
CI_YML
end
@@ -93,7 +93,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
let(:gitlab_ci_content) do
{ "stages" => %w(test),
"variables" => { "RANDOM" => "make sure this persists" },
- "include" => [{ "template" => "Security/Container-Scanning.gitlab-ci.yml" }] }
+ "include" => [{ "template" => "Jobs/Container-Scanning.gitlab-ci.yml" }] }
end
it 'generates the correct YML' do
@@ -106,7 +106,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
let(:gitlab_ci_content) do
{ "stages" => %w(test),
"variables" => { "RANDOM" => "make sure this persists" },
- "include" => { "template" => "Security/Container-Scanning.gitlab-ci.yml" } }
+ "include" => { "template" => "Jobs/Container-Scanning.gitlab-ci.yml" } }
end
it 'generates the correct YML' do
@@ -138,7 +138,7 @@ RSpec.describe Security::CiConfiguration::ContainerScanningBuildAction do
# DOCKER_USER: ...
# DOCKER_PASSWORD: ...
include:
- - template: Security/Container-Scanning.gitlab-ci.yml
+ - template: Jobs/Container-Scanning.gitlab-ci.yml
CI_YML
end
diff --git a/spec/lib/security/weak_passwords_spec.rb b/spec/lib/security/weak_passwords_spec.rb
index 9d12c352abf..afa9448e746 100644
--- a/spec/lib/security/weak_passwords_spec.rb
+++ b/spec/lib/security/weak_passwords_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Security::WeakPasswords do
+RSpec.describe Security::WeakPasswords, feature_category: :authentication_and_authorization do
describe "#weak_for_user?" do
using RSpec::Parameterized::TableSyntax
@@ -34,6 +34,7 @@ RSpec.describe Security::WeakPasswords do
"!@mCwEaKy" | true
"A1B2pass" | true
"A1B2C3jr" | false # jr is too short
+ "3e18a7f60a908e329958396d68131d39e1b66a03ea420725e2a0fce7cb17pass" | false # Password is >= 64 chars
# Predictable username substrings
"56d4ab689a" | true
diff --git a/spec/lib/serializers/json_spec.rb b/spec/lib/serializers/json_spec.rb
deleted file mode 100644
index 96a57cde056..00000000000
--- a/spec/lib/serializers/json_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'oj'
-
-RSpec.describe Serializers::Json do
- describe '.dump' do
- let(:obj) { { key: "value" } }
-
- subject { described_class.dump(obj) }
-
- it 'returns a hash' do
- is_expected.to eq(obj)
- end
- end
-
- describe '.load' do
- let(:data_string) { '{"key":"value","variables":[{"key":"VAR1","value":"VALUE1"}]}' }
- let(:data_hash) { Gitlab::Json.parse(data_string) }
-
- context 'when loading a hash' do
- subject { described_class.load(data_hash) }
-
- it 'decodes a string' do
- is_expected.to be_a(Hash)
- end
-
- it 'allows to access with symbols' do
- expect(subject[:key]).to eq('value')
- expect(subject[:variables].first[:key]).to eq('VAR1')
- end
-
- it 'allows to access with strings' do
- expect(subject["key"]).to eq('value')
- expect(subject["variables"].first["key"]).to eq('VAR1')
- end
- end
-
- context 'when loading a nil' do
- subject { described_class.load(nil) }
-
- it 'returns nil' do
- is_expected.to be_nil
- end
- end
- end
-end
diff --git a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
index 685ba0c31c7..ce971915174 100644
--- a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
@@ -47,44 +47,16 @@ RSpec.describe Sidebars::Projects::Menus::DeploymentsMenu do
end
end
- shared_examples 'split_operations_visibility_permissions FF disabled' do
- before do
- stub_feature_flags(split_operations_visibility_permissions: false)
- end
-
- it { is_expected.not_to be_nil }
-
- context 'and the feature is disabled' do
- before do
- project.update_attribute("#{item_id}_access_level", 'disabled')
- end
-
- it { is_expected.not_to be_nil }
- end
-
- context 'and operations is disabled' do
- before do
- project.update_attribute(:operations_access_level, 'disabled')
- end
-
- it do
- is_expected.to be_nil if [:environments, :feature_flags].include?(item_id)
- end
- end
- end
-
describe 'Feature Flags' do
let(:item_id) { :feature_flags }
it_behaves_like 'access rights checks'
- it_behaves_like 'split_operations_visibility_permissions FF disabled'
end
describe 'Environments' do
let(:item_id) { :environments }
it_behaves_like 'access rights checks'
- it_behaves_like 'split_operations_visibility_permissions FF disabled'
end
describe 'Releases' do
diff --git a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
index 64408ac3b88..116948b7cb0 100644
--- a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
@@ -31,43 +31,18 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
let(:enabled) { Featurable::PRIVATE }
let(:disabled) { Featurable::DISABLED }
- where(:operations_access_level, :infrastructure_access_level, :render) do
- ref(:disabled) | ref(:enabled) | true
- ref(:disabled) | ref(:disabled) | false
- ref(:enabled) | ref(:enabled) | true
- ref(:enabled) | ref(:disabled) | false
+ where(:infrastructure_access_level, :render) do
+ ref(:enabled) | true
+ ref(:disabled) | false
end
with_them do
it 'renders based on the infrastructure access level' do
- project.project_feature.update!(operations_access_level: operations_access_level)
project.project_feature.update!(infrastructure_access_level: infrastructure_access_level)
expect(subject.render?).to be render
end
end
-
- context 'when `split_operations_visibility_permissions` feature flag is disabled' do
- before do
- stub_feature_flags(split_operations_visibility_permissions: false)
- end
-
- where(:operations_access_level, :infrastructure_access_level, :render) do
- ref(:disabled) | ref(:enabled) | false
- ref(:disabled) | ref(:disabled) | false
- ref(:enabled) | ref(:enabled) | true
- ref(:enabled) | ref(:disabled) | true
- end
-
- with_them do
- it 'renders based on the operations access level' do
- project.project_feature.update!(operations_access_level: operations_access_level)
- project.project_feature.update!(infrastructure_access_level: infrastructure_access_level)
-
- expect(subject.render?).to be render
- end
- end
- end
end
end
diff --git a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
index f6a8dd7367d..a1e6ae13e68 100644
--- a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
@@ -16,40 +16,30 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu do
let(:enabled) { Featurable::PRIVATE }
let(:disabled) { Featurable::DISABLED }
- where(:flag_enabled, :operations_access_level, :monitor_level, :render) do
- true | ref(:disabled) | ref(:enabled) | true
- true | ref(:disabled) | ref(:disabled) | false
- true | ref(:enabled) | ref(:enabled) | true
- true | ref(:enabled) | ref(:disabled) | false
- false | ref(:disabled) | ref(:enabled) | false
- false | ref(:disabled) | ref(:disabled) | false
- false | ref(:enabled) | ref(:enabled) | true
- false | ref(:enabled) | ref(:disabled) | true
+ where(:monitor_level, :render) do
+ ref(:enabled) | true
+ ref(:disabled) | false
end
with_them do
it 'renders when expected to' do
- stub_feature_flags(split_operations_visibility_permissions: flag_enabled)
- project.project_feature.update!(operations_access_level: operations_access_level)
project.project_feature.update!(monitor_access_level: monitor_level)
expect(subject.render?).to be render
end
end
- context 'when operation feature is enabled' do
- context 'when menu does not have any renderable menu items' do
- it 'returns false' do
- allow(subject).to receive(:has_renderable_items?).and_return(false)
+ context 'when menu does not have any renderable menu items' do
+ it 'returns false' do
+ allow(subject).to receive(:has_renderable_items?).and_return(false)
- expect(subject.render?).to be false
- end
+ expect(subject.render?).to be false
end
+ end
- context 'when menu has menu items' do
- it 'returns true' do
- expect(subject.render?).to be true
- end
+ context 'when menu has menu items' do
+ it 'returns true' do
+ expect(subject.render?).to be true
end
end
end
diff --git a/spec/lib/sidebars/projects/menus/repository_menu_spec.rb b/spec/lib/sidebars/projects/menus/repository_menu_spec.rb
index f26433306b6..e7aa2b7edca 100644
--- a/spec/lib/sidebars/projects/menus/repository_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/repository_menu_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Sidebars::Projects::Menus::RepositoryMenu do
+RSpec.describe Sidebars::Projects::Menus::RepositoryMenu, feature_category: :source_code_management do
let_it_be(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
@@ -36,12 +36,68 @@ RSpec.describe Sidebars::Projects::Menus::RepositoryMenu do
end
context 'for menu items' do
- subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } }
+ shared_examples_for 'repository menu item link for' do |item_id|
+ let(:ref) { 'master' }
+ let(:item_id) { item_id }
+ subject { described_class.new(context).renderable_items.find { |e| e.item_id == item_id }.link }
+
+ using RSpec::Parameterized::TableSyntax
+
+ let(:context) do
+ Sidebars::Projects::Context.new(current_user: user, container: project, current_ref: ref,
+ ref_type: ref_type)
+ end
+
+ where(:feature_flag_enabled, :ref_type, :link) do
+ true | nil | lazy { "#{route}?ref_type=heads" }
+ true | 'heads' | lazy { "#{route}?ref_type=heads" }
+ true | 'tags' | lazy { "#{route}?ref_type=tags" }
+ false | nil | lazy { route }
+ false | 'heads' | lazy { route }
+ false | 'tags' | lazy { route }
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(use_ref_type_parameter: feature_flag_enabled)
+ end
+
+ it 'has a link with the fully qualifed ref route' do
+ expect(subject).to eq(link)
+ end
+ end
+
+ context 'when ref is not the default' do
+ let(:ref) { 'nonmain' }
+
+ context 'and ref_type is not provided' do
+ let(:ref_type) { nil }
+
+ it { is_expected.to eq(route) }
+ end
+
+ context 'and ref_type is provided' do
+ let(:ref_type) { 'heads' }
+
+ it { is_expected.to eq("#{route}?ref_type=heads") }
+ end
+ end
+ end
+
+ describe 'Commits' do
+ let_it_be(:item_id) { :commits }
+
+ it_behaves_like 'repository menu item link for', :commits do
+ let(:route) { "/#{project.full_path}/-/commits/#{ref}" }
+ end
+ end
describe 'Contributors' do
let_it_be(:item_id) { :contributors }
context 'when analytics is disabled' do
+ subject { described_class.new(context).renderable_items.find { |e| e.item_id == item_id } }
+
before do
project.project_feature.update!(analytics_access_level: ProjectFeature::DISABLED)
end
@@ -54,7 +110,15 @@ RSpec.describe Sidebars::Projects::Menus::RepositoryMenu do
project.project_feature.update!(analytics_access_level: ProjectFeature::ENABLED)
end
- it { is_expected.not_to be_nil }
+ it_behaves_like 'repository menu item link for', :contributors do
+ let(:route) { "/#{project.full_path}/-/graphs/#{ref}" }
+ end
+ end
+ end
+
+ describe 'Network' do
+ it_behaves_like 'repository menu item link for', :graphs do
+ let(:route) { "/#{project.full_path}/-/network/#{ref}" }
end
end
end
diff --git a/spec/lib/system_check/app/gitlab_cable_config_exists_check_spec.rb b/spec/lib/system_check/app/gitlab_cable_config_exists_check_spec.rb
new file mode 100644
index 00000000000..8e127bb715c
--- /dev/null
+++ b/spec/lib/system_check/app/gitlab_cable_config_exists_check_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SystemCheck::App::GitlabCableConfigExistsCheck, feature_category: :redis do
+ subject(:system_check) { described_class.new }
+
+ describe '#check?' do
+ subject { system_check.check? }
+
+ context 'when config/cable.yml exists' do
+ before do
+ allow(File).to receive(:exist?).and_return(true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when config/cable.yml does not exist' do
+ before do
+ allow(File).to receive(:exist?).and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+end
diff --git a/spec/lib/system_check/app/gitlab_resque_config_exists_check_spec.rb b/spec/lib/system_check/app/gitlab_resque_config_exists_check_spec.rb
new file mode 100644
index 00000000000..d2e5dec7460
--- /dev/null
+++ b/spec/lib/system_check/app/gitlab_resque_config_exists_check_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SystemCheck::App::GitlabResqueConfigExistsCheck, feature_category: :redis do
+ subject(:system_check) { described_class.new }
+
+ describe '#check?' do
+ subject { system_check.check? }
+
+ context 'when config/resque.yml exists' do
+ before do
+ allow(File).to receive(:exist?).and_return(true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when config/resque.yml does not exist' do
+ before do
+ allow(File).to receive(:exist?).and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+end
diff --git a/spec/lib/system_check/base_check_spec.rb b/spec/lib/system_check/base_check_spec.rb
index 241c3b33777..168bda07791 100644
--- a/spec/lib/system_check/base_check_spec.rb
+++ b/spec/lib/system_check/base_check_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe SystemCheck::BaseCheck do
it 'responds to Gitlab::TaskHelpers methods' do
expect(subject).to respond_to :ask_to_continue, :os_name, :prompt, :run_and_match, :run_command,
- :run_command!, :uid_for, :gid_for, :gitlab_user, :gitlab_user?, :warn_user_is_not_gitlab, :all_repos,
+ :run_command!, :uid_for, :gid_for, :gitlab_user, :gitlab_user?, :warn_user_is_not_gitlab,
:repository_storage_paths_args, :user_home, :checkout_or_clone_version, :clone_repo, :checkout_version
end
end
diff --git a/spec/lib/system_check/sidekiq_check_spec.rb b/spec/lib/system_check/sidekiq_check_spec.rb
index c2f61e0e4b7..ff4eece8f7c 100644
--- a/spec/lib/system_check/sidekiq_check_spec.rb
+++ b/spec/lib/system_check/sidekiq_check_spec.rb
@@ -37,45 +37,53 @@ RSpec.describe SystemCheck::SidekiqCheck do
)
end
- it 'succeeds when one cluster process and one or more worker processes are running' do
- stub_ps_output <<~PS
- root 2193947 0.9 0.1 146564 18104 ? Ssl 17:34 0:00 ruby bin/sidekiq-cluster * -P ...
- root 2193955 92.2 3.1 4675972 515516 ? Sl 17:34 0:13 sidekiq 5.2.9 ...
- root 2193956 92.2 3.1 4675972 515516 ? Sl 17:34 0:13 sidekiq 5.2.9 ...
- PS
-
- expect_check_output <<~OUTPUT
- Running? ... yes
- Number of Sidekiq processes (cluster/worker) ... 1/2
- OUTPUT
- end
-
- # TODO: Running without a cluster is deprecated and will be removed in GitLab 14.0
- # https://gitlab.com/gitlab-org/gitlab/-/issues/323225
- context 'when running without a cluster' do
- it 'fails when more than one worker process is running' do
+ context 'when only a worker process is running' do
+ before do
stub_ps_output <<~PS
root 2193955 92.2 3.1 4675972 515516 ? Sl 17:34 0:13 sidekiq 5.2.9 ...
- root 2193956 92.2 3.1 4675972 515516 ? Sl 17:34 0:13 sidekiq 5.2.9 ...
PS
+ end
- expect_check_output include(
- 'Running? ... yes',
- 'Number of Sidekiq processes (cluster/worker) ... 0/2',
- 'Please fix the error above and rerun the checks.'
- )
+ it 'fails with the right message for systemd' do
+ allow(File).to receive(:symlink?).with(described_class::SYSTEMD_UNIT_PATH).and_return(true)
+
+ expect_check_output <<~OUTPUT
+ Running? ... yes
+ Number of Sidekiq processes (cluster/worker) ... 0/1
+ Try fixing it:
+ sudo systemctl restart gitlab-sidekiq.service
+ Please fix the error above and rerun the checks.
+ OUTPUT
end
- it 'succeeds when one worker process is running' do
- stub_ps_output <<~PS
- root 2193955 92.2 3.1 4675972 515516 ? Sl 17:34 0:13 sidekiq 5.2.9 ...
- PS
+ it 'fails with the right message for sysvinit' do
+ allow(File).to receive(:symlink?).with(described_class::SYSTEMD_UNIT_PATH).and_return(false)
+ allow(subject).to receive(:gitlab_user).and_return('git')
expect_check_output <<~OUTPUT
Running? ... yes
Number of Sidekiq processes (cluster/worker) ... 0/1
+ Try fixing it:
+ sudo service gitlab stop
+ sudo pkill -u git -f sidekiq
+ sleep 10 && sudo pkill -9 -u git -f sidekiq
+ sudo service gitlab start
+ Please fix the error above and rerun the checks.
OUTPUT
end
end
+
+ it 'succeeds when one cluster process and one or more worker processes are running' do
+ stub_ps_output <<~PS
+ root 2193947 0.9 0.1 146564 18104 ? Ssl 17:34 0:00 ruby bin/sidekiq-cluster * -P ...
+ root 2193955 92.2 3.1 4675972 515516 ? Sl 17:34 0:13 sidekiq 5.2.9 ...
+ root 2193956 92.2 3.1 4675972 515516 ? Sl 17:34 0:13 sidekiq 5.2.9 ...
+ PS
+
+ expect_check_output <<~OUTPUT
+ Running? ... yes
+ Number of Sidekiq processes (cluster/worker) ... 1/2
+ OUTPUT
+ end
end
end
diff --git a/spec/lib/version_check_spec.rb b/spec/lib/version_check_spec.rb
index 1803dd66ba7..4aa8975b7cf 100644
--- a/spec/lib/version_check_spec.rb
+++ b/spec/lib/version_check_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe VersionCheck do
+RSpec.describe VersionCheck, :use_clean_rails_memory_store_caching do
+ include ReactiveCachingHelpers
+
describe '.url' do
it 'returns the correct URL' do
expect(described_class.url).to match(%r{\A#{Regexp.escape(described_class.host)}/check\.json\?gitlab_info=\w+})
@@ -24,13 +26,25 @@ RSpec.describe VersionCheck do
end
describe '#calculate_reactive_cache' do
- context 'response code is 200' do
+ context 'response code is 200 with valid body' do
before do
stub_request(:get, described_class.url).to_return(status: 200, body: '{ "status": "success" }', headers: {})
end
it 'returns the response object' do
- expect(described_class.new.calculate_reactive_cache).to eq("{ \"status\": \"success\" }")
+ expect(described_class.new.calculate_reactive_cache).to eq({ "status" => "success" })
+ end
+ end
+
+ context 'response code is 200 with invalid body' do
+ before do
+ stub_request(:get, described_class.url).to_return(status: 200, body: '{ "invalid: json" }', headers: {})
+ end
+
+ it 'returns an error hash' do
+ expect(described_class.new.calculate_reactive_cache).to eq(
+ { error: 'parsing version check response failed', status: 200 }
+ )
end
end
@@ -39,38 +53,61 @@ RSpec.describe VersionCheck do
stub_request(:get, described_class.url).to_return(status: 500, body: nil, headers: {})
end
- it 'returns nil' do
- expect(described_class.new.calculate_reactive_cache).to be(nil)
+ it 'returns an error hash' do
+ expect(described_class.new.calculate_reactive_cache).to eq({ error: 'version check failed', status: 500 })
end
end
end
describe '#response' do
- context 'cache returns value' do
- let(:response) { { "severity" => "success" }.to_json }
-
+ # see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106254
+ context "with old string value in cache" do
before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:with_reactive_cache).and_return(response)
- end
+ old_version_check = described_class.new
+ allow(old_version_check).to receive(:id).and_return(Gitlab::VERSION)
+ write_reactive_cache(old_version_check,
+ "{\"latest_stable_versions\":[],\"latest_version\":\"15.6.2\",\"severity\":\"success\",\"details\":\"\"}"
+ )
end
- it 'returns the response object' do
- expect(described_class.new.response).to be(response)
+ it 'returns nil' do
+ version_check = described_class.new
+ expect(version_check.response).to be_nil
end
end
- context 'cache returns nil' do
- let(:response) { nil }
+ # see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106254
+ context "with non-hash value in cache" do
+ it 'returns nil and invalidates the reactive cache' do
+ version_check = described_class.new
+ stub_reactive_cache(version_check,
+ "{\"latest_stable_versions\":[],\"latest_version\":\"15.6.2\",\"severity\":\"success\",\"details\":\"\"}"
+ )
- before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:with_reactive_cache).and_return(response)
- end
+ expect(version_check).to receive(:refresh_reactive_cache!).and_call_original
+ expect(version_check.response).to be_nil
+ expect(read_reactive_cache(version_check)).to be_nil
end
+ end
- it 'returns nil' do
- expect(described_class.new.response).to be(nil)
+ context 'cache returns value' do
+ it 'returns the response object' do
+ version_check = described_class.new
+ data = { status: 'success' }
+ stub_reactive_cache(version_check, data)
+
+ expect(version_check.response).to eq(data)
+ end
+ end
+
+ context 'cache returns error' do
+ it 'returns nil and invalidates the reactive cache' do
+ version_check = described_class.new
+ stub_reactive_cache(version_check, error: 'version check failed')
+
+ expect(version_check).to receive(:refresh_reactive_cache!).and_call_original
+ expect(version_check.response).to be_nil
+ expect(read_reactive_cache(version_check)).to be_nil
end
end
end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 767eddb7f98..cdc298d685e 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -152,7 +152,7 @@ RSpec.describe Emails::Profile do
end
it 'includes the email reason' do
- is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>}
+ is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost</a>}
end
end
end
@@ -188,7 +188,7 @@ RSpec.describe Emails::Profile do
end
it 'includes the email reason' do
- is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>}
+ is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost</a>}
end
context 'with User does not exist' do
@@ -223,7 +223,7 @@ RSpec.describe Emails::Profile do
end
it 'includes the email reason' do
- is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>}
+ is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost</a>}
end
end
@@ -269,8 +269,40 @@ RSpec.describe Emails::Profile do
is_expected.to have_body_text /#{token.name}/
end
+ it 'wont include the revocation reason' do
+ is_expected.not_to have_body_text %r{We found your token in a public project and have automatically revoked it to protect your account.$}
+ end
+
+ it 'includes the email reason' do
+ is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost</a>}
+ end
+ end
+
+ context 'when source is provided' do
+ subject { Notify.access_token_revoked_email(user, token.name, 'secret_detection') }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+
+ it 'is sent to the user' do
+ is_expected.to deliver_to user.email
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /^A personal access token has been revoked$/i
+ end
+
+ it 'provides the names of the token' do
+ is_expected.to have_body_text /#{token.name}/
+ end
+
+ it 'includes the revocation reason' do
+ is_expected.to have_body_text %r{We found your token in a public project and have automatically revoked it to protect your account.$}
+ end
+
it 'includes the email reason' do
- is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>}
+ is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost</a>}
end
end
end
@@ -296,7 +328,7 @@ RSpec.describe Emails::Profile do
end
shared_examples 'includes the email reason' do
- it { is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>} }
+ it { is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost</a>} }
end
shared_examples 'valid use case' do
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 5733e892d2a..684fe9bb9cf 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -419,15 +419,15 @@ RSpec.describe Notify do
end
it 'includes the reason in the footer' do
- text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(reason: NotificationReason::ASSIGNED, format: :html)
+ text = EmailsHelper.instance_method(:notification_reason_text).bind_call(self, reason: NotificationReason::ASSIGNED, format: :html)
is_expected.to have_body_text(text)
new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::MENTIONED)
- text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(reason: NotificationReason::MENTIONED, format: :html)
+ text = EmailsHelper.instance_method(:notification_reason_text).bind_call(self, reason: NotificationReason::MENTIONED, format: :html)
expect(new_subject).to have_body_text(text)
new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, nil)
- text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(format: :html)
+ text = EmailsHelper.instance_method(:notification_reason_text).bind_call(self, format: :html)
expect(new_subject).to have_body_text(text)
end
end
@@ -649,16 +649,20 @@ RSpec.describe Notify do
end
context 'the model has no namespace' do
- class TopLevelThing
- include Referable
- include Noteable
+ before do
+ stub_const('TopLevelThing', Class.new)
- def to_reference(*_args)
- 'tlt-ref'
- end
+ TopLevelThing.class_eval do
+ include Referable
+ include Noteable
- def id
- 'tlt-id'
+ def to_reference(*_args)
+ 'tlt-ref'
+ end
+
+ def id
+ 'tlt-id'
+ end
end
end
@@ -672,8 +676,10 @@ RSpec.describe Notify do
end
context 'the model has a namespace' do
- module Namespaced
- class Thing
+ before do
+ stub_const('Namespaced::Thing', Class.new)
+
+ Namespaced::Thing.class_eval do
include Referable
include Noteable
diff --git a/spec/metrics_server/metrics_server_spec.rb b/spec/metrics_server/metrics_server_spec.rb
index c7716184d48..58577d4d633 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 do # rubocop:disable RSpec/FilePath
+RSpec.describe MetricsServer, feature_category: :application_performance do # rubocop:disable RSpec/FilePath
let(:prometheus_config) { ::Prometheus::Client.configuration }
let(:metrics_dir) { Dir.mktmpdir }
@@ -118,6 +118,7 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
let(:expected_port) { target == 'puma' ? '8083' : '8082' }
let(:expected_env) do
{
+ 'GOGC' => '10',
'GME_MMAP_METRICS_DIR' => metrics_dir,
'GME_PROBES' => 'self,mmap',
'GME_SERVER_HOST' => 'localhost',
diff --git a/spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb b/spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb
index 1f18f7e581a..18aa8e92560 100644
--- a/spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb
+++ b/spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb
@@ -3,12 +3,13 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillTotalTupleCountForBatchedMigrations, :migration, schema: 20210406140057 do
- let_it_be(:table_name) { 'projects' }
+RSpec.describe BackfillTotalTupleCountForBatchedMigrations, :migration, schema: 20210406140057,
+ feature_category: :database do
+ let!(:table_name) { 'projects' }
- let_it_be(:migrations) { table(:batched_background_migrations) }
+ let!(:migrations) { table(:batched_background_migrations) }
- let_it_be(:migration) do
+ let!(:migration) do
migrations.create!(
created_at: Time.now,
updated_at: Time.now,
diff --git a/spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb b/spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb
index faf440eb117..258bf7a3e69 100644
--- a/spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb
+++ b/spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb
@@ -3,33 +3,33 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleDropInvalidVulnerabilities, :migration do
- let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
- let_it_be(:users) { table(:users) }
- let_it_be(:user) { create_user! }
- let_it_be(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+RSpec.describe ScheduleDropInvalidVulnerabilities, :migration, feature_category: :value_stream_management do
+ let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let!(:users) { table(:users) }
+ let!(:user) { create_user! }
+ let!(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
- let_it_be(:scanners) { table(:vulnerability_scanners) }
- let_it_be(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
- let_it_be(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
+ let!(:scanners) { table(:vulnerability_scanners) }
+ let!(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let!(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
- let_it_be(:vulnerabilities) { table(:vulnerabilities) }
- let_it_be(:vulnerability_with_finding) do
+ let!(:vulnerabilities) { table(:vulnerabilities) }
+ let!(:vulnerability_with_finding) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
- let_it_be(:vulnerability_without_finding) do
+ let!(:vulnerability_without_finding) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
- let_it_be(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
- let_it_be(:primary_identifier) do
+ let!(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+ let!(:primary_identifier) do
vulnerability_identifiers.create!(
project_id: project.id,
external_type: 'uuid-v5',
@@ -38,8 +38,8 @@ RSpec.describe ScheduleDropInvalidVulnerabilities, :migration do
name: 'Identifier for UUIDv5')
end
- let_it_be(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
- let_it_be(:finding) do
+ let!(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let!(:finding) do
create_finding!(
vulnerability_id: vulnerability_with_finding.id,
project_id: project.id,
@@ -82,7 +82,7 @@ RSpec.describe ScheduleDropInvalidVulnerabilities, :migration do
vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
- metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
vulnerabilities_findings.create!(
vulnerability_id: vulnerability_id,
project_id: project_id,
diff --git a/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb b/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb
index ed18820ec8d..688fc5eb23a 100644
--- a/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb
+++ b/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe CopyAdoptionSnapshotNamespace, :migration, schema: 20210430124630 do
+RSpec.describe CopyAdoptionSnapshotNamespace, :migration, schema: 20210430124630, feature_category: :devops_reports do
let(:namespaces_table) { table(:namespaces) }
let(:segments_table) { table(:analytics_devops_adoption_segments) }
let(:snapshots_table) { table(:analytics_devops_adoption_snapshots) }
diff --git a/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb b/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb
index 25dfaa2e314..0fb3029ec6a 100644
--- a/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb
+++ b/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe CopyAdoptionSegmentsNamespace, :migration do
+RSpec.describe CopyAdoptionSegmentsNamespace, :migration, feature_category: :devops_reports do
let(:namespaces_table) { table(:namespaces) }
let(:segments_table) { table(:analytics_devops_adoption_segments) }
diff --git a/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb b/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb
index 187b9115ba7..07a90c2d276 100644
--- a/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb
+++ b/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddProjectValueStreamIdToProjectStages, schema: 20210503105022 do
+RSpec.describe AddProjectValueStreamIdToProjectStages, schema: 20210503105022,
+ feature_category: :value_stream_management do
let(:stages) { table(:analytics_cycle_analytics_project_stages) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb b/spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb
index dd557c833f3..b514c92c52d 100644
--- a/spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb
+++ b/spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb
@@ -3,35 +3,35 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleDropInvalidVulnerabilities2, :migration do
- let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
+RSpec.describe ScheduleDropInvalidVulnerabilities2, :migration, feature_category: :value_stream_management do
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
- let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
- let_it_be(:users) { table(:users) }
- let_it_be(:user) { create_user! }
- let_it_be(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let!(:users) { table(:users) }
+ let!(:user) { create_user! }
+ let!(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
- let_it_be(:scanners) { table(:vulnerability_scanners) }
- let_it_be(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
- let_it_be(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
+ let!(:scanners) { table(:vulnerability_scanners) }
+ let!(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let!(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
- let_it_be(:vulnerabilities) { table(:vulnerabilities) }
- let_it_be(:vulnerability_with_finding) do
+ let!(:vulnerabilities) { table(:vulnerabilities) }
+ let!(:vulnerability_with_finding) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
- let_it_be(:vulnerability_without_finding) do
+ let!(:vulnerability_without_finding) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
- let_it_be(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
- let_it_be(:primary_identifier) do
+ let!(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+ let!(:primary_identifier) do
vulnerability_identifiers.create!(
project_id: project.id,
external_type: 'uuid-v5',
@@ -40,8 +40,8 @@ RSpec.describe ScheduleDropInvalidVulnerabilities2, :migration do
name: 'Identifier for UUIDv5')
end
- let_it_be(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
- let_it_be(:finding) do
+ let!(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let!(:finding) do
create_finding!(
vulnerability_id: vulnerability_with_finding.id,
project_id: project.id,
@@ -88,7 +88,7 @@ RSpec.describe ScheduleDropInvalidVulnerabilities2, :migration do
vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
- metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
vulnerabilities_findings.create!(
vulnerability_id: vulnerability_id,
project_id: project_id,
diff --git a/spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb b/spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb
index 4ac4af19eb9..8a76f0847e9 100644
--- a/spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb
+++ b/spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleCleanupOrphanedLfsObjectsProjects, schema: 20210511165250 do
+RSpec.describe ScheduleCleanupOrphanedLfsObjectsProjects, schema: 20210511165250, feature_category: :git_lfs do
let(:lfs_objects_projects) { table(:lfs_objects_projects) }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb b/spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb
index fa4b747aaed..24a71e48035 100644
--- a/spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb
+++ b/spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe FixTotalStageInVsa, :migration, schema: 20210518001450 do
+RSpec.describe FixTotalStageInVsa, :migration, schema: 20210518001450, feature_category: :devops_reports do
let(:namespaces) { table(:namespaces) }
let(:group_value_streams) { table(:analytics_cycle_analytics_group_value_streams) }
let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
diff --git a/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb b/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb
index 8d45f571969..592497805de 100644
--- a/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb
+++ b/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe GroupProtectedEnvironmentsAddIndexAndConstraint do
+RSpec.describe GroupProtectedEnvironmentsAddIndexAndConstraint, feature_category: :continuous_delivery do
let(:migration) { described_class.new }
let(:protected_environments) { table(:protected_environments) }
let(:group) { table(:namespaces).create!(name: 'group', path: 'group') }
diff --git a/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb b/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb
index 14aa4fe8da7..706e0b14492 100644
--- a/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb
+++ b/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe RemoveBuildsEmailServiceFromServices do
+RSpec.describe RemoveBuildsEmailServiceFromServices, feature_category: :navigation do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:services) { table(:services) }
diff --git a/spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb b/spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb
index 17599e75947..300c43b9133 100644
--- a/spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb
+++ b/spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe DeleteLegacyOperationsFeatureFlags do
+RSpec.describe DeleteLegacyOperationsFeatureFlags, feature_category: :feature_flags do
let(:namespace) { table(:namespaces).create!(name: 'foo', path: 'bar') }
let(:project) { table(:projects).create!(namespace_id: namespace.id) }
let(:issue) { table(:issues).create!(id: 123, project_id: project.id) }
diff --git a/spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb b/spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb
index 8dfeacc4774..baa5fd7efbd 100644
--- a/spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb
+++ b/spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe CascadeDeleteFreezePeriods, :suppress_gitlab_schemas_validate_connection do
+RSpec.describe CascadeDeleteFreezePeriods, :suppress_gitlab_schemas_validate_connection, feature_category: :continuous_delivery do
let(:namespace) { table(:namespaces).create!(name: 'deploy_freeze', path: 'deploy_freeze') }
let(:project) { table(:projects).create!(id: 1, namespace_id: namespace.id) }
let(:freeze_periods) { table(:ci_freeze_periods) }
diff --git a/spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb b/spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb
index 7a281611650..604504d2206 100644
--- a/spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb
+++ b/spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe RescheduleMergeRequestDiffUsersBackgroundMigration, :migration do
+RSpec.describe RescheduleMergeRequestDiffUsersBackgroundMigration, :migration, feature_category: :code_review do
let(:migration) { described_class.new }
describe '#up' do
diff --git a/spec/migrations/20210713042000_fix_ci_sources_pipelines_index_names_spec.rb b/spec/migrations/20210713042000_fix_ci_sources_pipelines_index_names_spec.rb
index adec1e05533..6761b69aed5 100644
--- a/spec/migrations/20210713042000_fix_ci_sources_pipelines_index_names_spec.rb
+++ b/spec/migrations/20210713042000_fix_ci_sources_pipelines_index_names_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe FixCiSourcesPipelinesIndexNames, :migration do
+RSpec.describe FixCiSourcesPipelinesIndexNames, :migration, feature_category: :continuous_integration do
def validate_foreign_keys_and_index!
aggregate_failures do
expect(subject.foreign_key_exists?(:ci_sources_pipelines, :ci_builds, column: :source_job_id, name: 'fk_be5624bf37')).to be_truthy
diff --git a/spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb b/spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb
index 63802acceb5..5674efbf187 100644
--- a/spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb
+++ b/spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe UpdateIssuableSlasWhereIssueClosed, :migration do
+RSpec.describe UpdateIssuableSlasWhereIssueClosed, :migration, feature_category: :team_planning do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:issues) { table(:issues) }
diff --git a/spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb b/spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb
index 94af2bb1e9a..098dd647b27 100644
--- a/spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb
+++ b/spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
require_migration!
-RSpec.describe OperationsFeatureFlagsCorrectFlexibleRolloutValues, :migration do
- let_it_be(:strategies) { table(:operations_strategies) }
+RSpec.describe OperationsFeatureFlagsCorrectFlexibleRolloutValues, :migration, feature_category: :feature_flags do
+ let!(:strategies) { table(:operations_strategies) }
let(:namespace) { table(:namespaces).create!(name: 'feature_flag', path: 'feature_flag') }
let(:project) { table(:projects).create!(namespace_id: namespace.id) }
diff --git a/spec/migrations/20210804150320_create_base_work_item_types_spec.rb b/spec/migrations/20210804150320_create_base_work_item_types_spec.rb
index ae510826fe1..5626b885626 100644
--- a/spec/migrations/20210804150320_create_base_work_item_types_spec.rb
+++ b/spec/migrations/20210804150320_create_base_work_item_types_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe CreateBaseWorkItemTypes, :migration do
+RSpec.describe CreateBaseWorkItemTypes, :migration, feature_category: :team_planning do
include MigrationHelpers::WorkItemTypesHelper
- let_it_be(:work_item_types) { table(:work_item_types) }
+ let!(:work_item_types) { table(:work_item_types) }
let(:base_types) do
{
diff --git a/spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb b/spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb
index b1885b96adb..d18673db757 100644
--- a/spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb
+++ b/spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe UpdateTrialPlansCiDailyPipelineScheduleTriggers, :migration do
+RSpec.describe UpdateTrialPlansCiDailyPipelineScheduleTriggers, :migration, feature_category: :purchase do
let!(:plans) { table(:plans) }
let!(:plan_limits) { table(:plan_limits) }
let!(:premium_trial_plan) { plans.create!(name: 'premium_trial', title: 'Premium Trial') }
diff --git a/spec/migrations/20210811122206_update_external_project_bots_spec.rb b/spec/migrations/20210811122206_update_external_project_bots_spec.rb
index 365fb8e3218..aa0bce63865 100644
--- a/spec/migrations/20210811122206_update_external_project_bots_spec.rb
+++ b/spec/migrations/20210811122206_update_external_project_bots_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe UpdateExternalProjectBots, :migration do
+RSpec.describe UpdateExternalProjectBots, :migration, feature_category: :users do
def create_user(**extra_options)
defaults = { projects_limit: 0, email: "#{extra_options[:username]}@example.com" }
diff --git a/spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb b/spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb
index c88f94c6426..fcc2e1657d0 100644
--- a/spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb
+++ b/spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!('remove_duplicate_project_authorizations')
-RSpec.describe RemoveDuplicateProjectAuthorizations, :migration do
+RSpec.describe RemoveDuplicateProjectAuthorizations, :migration, feature_category: :authentication_and_authorization do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb b/spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb
index 4ad4bea058b..e48f933ad5f 100644
--- a/spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb
+++ b/spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe DropTemporaryColumnsAndTriggersForCiBuildsRunnerSession, :migration do
+RSpec.describe DropTemporaryColumnsAndTriggersForCiBuildsRunnerSession, :migration, feature_category: :runner do
let(:ci_builds_runner_session_table) { table(:ci_builds_runner_session) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb b/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb
index 552602983d9..2a19dc025a7 100644
--- a/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb
+++ b/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe UpsertBaseWorkItemTypes, :migration do
+RSpec.describe UpsertBaseWorkItemTypes, :migration, feature_category: :team_planning do
include MigrationHelpers::WorkItemTypesHelper
- let_it_be(:work_item_types) { table(:work_item_types) }
+ let!(:work_item_types) { table(:work_item_types) }
let(:base_types) do
{
diff --git a/spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb b/spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb
index 4ec3c5b7211..0d89851cac1 100644
--- a/spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb
+++ b/spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe DropTemporaryColumnsAndTriggersForCiBuildNeeds do
+RSpec.describe DropTemporaryColumnsAndTriggersForCiBuildNeeds, feature_category: :pipeline_authoring do
let(:ci_build_needs_table) { table(:ci_build_needs) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20210906100316_drop_temporary_columns_and_triggers_for_ci_build_trace_chunks_spec.rb b/spec/migrations/20210906100316_drop_temporary_columns_and_triggers_for_ci_build_trace_chunks_spec.rb
index f1408e4ecab..eef4c7bc9fd 100644
--- a/spec/migrations/20210906100316_drop_temporary_columns_and_triggers_for_ci_build_trace_chunks_spec.rb
+++ b/spec/migrations/20210906100316_drop_temporary_columns_and_triggers_for_ci_build_trace_chunks_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe DropTemporaryColumnsAndTriggersForCiBuildTraceChunks do
+RSpec.describe DropTemporaryColumnsAndTriggersForCiBuildTraceChunks, feature_category: :continuous_integration do
let(:ci_build_trace_chunks_table) { table(:ci_build_trace_chunks) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20210906130643_drop_temporary_columns_and_triggers_for_taggings_spec.rb b/spec/migrations/20210906130643_drop_temporary_columns_and_triggers_for_taggings_spec.rb
index e4385e501b2..208cbac2ae9 100644
--- a/spec/migrations/20210906130643_drop_temporary_columns_and_triggers_for_taggings_spec.rb
+++ b/spec/migrations/20210906130643_drop_temporary_columns_and_triggers_for_taggings_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe DropTemporaryColumnsAndTriggersForTaggings do
+RSpec.describe DropTemporaryColumnsAndTriggersForTaggings, feature_category: :continuous_integration do
let(:taggings_table) { table(:taggings) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20210907013944_cleanup_bigint_conversion_for_ci_builds_metadata_spec.rb b/spec/migrations/20210907013944_cleanup_bigint_conversion_for_ci_builds_metadata_spec.rb
index 194832fbc43..63664803fba 100644
--- a/spec/migrations/20210907013944_cleanup_bigint_conversion_for_ci_builds_metadata_spec.rb
+++ b/spec/migrations/20210907013944_cleanup_bigint_conversion_for_ci_builds_metadata_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupBigintConversionForCiBuildsMetadata do
+RSpec.describe CleanupBigintConversionForCiBuildsMetadata, feature_category: :continuous_integration do
let(:ci_builds_metadata) { table(:ci_builds_metadata) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb b/spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb
index c0f56da7b4f..663b90f3fa7 100644
--- a/spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb
+++ b/spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe FinalizeCiBuildsBigintConversion, :migration, schema: 20210907182359 do
+RSpec.describe FinalizeCiBuildsBigintConversion, :migration, schema: 20210907182359, feature_category: :continuous_integration do
context 'with an unexpected FK fk_3f0c88d7dc' do
it 'removes the FK and migrates successfully' do
# Add the unexpected FK
diff --git a/spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb b/spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb
index 69ee10eb0d1..e9d34fad76d 100644
--- a/spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb
+++ b/spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe UpdateReportTypeForExistingApprovalProjectRules, :migration do
+RSpec.describe UpdateReportTypeForExistingApprovalProjectRules, :migration, feature_category: :source_code_management do
using RSpec::Parameterized::TableSyntax
let(:group) { table(:namespaces).create!(name: 'user', path: 'user') }
diff --git a/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb b/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb
index 2b755dfe11c..a9a814f9a48 100644
--- a/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb
+++ b/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupOrphanProjectAccessTokens, :migration do
+RSpec.describe CleanupOrphanProjectAccessTokens, :migration, feature_category: :users do
def create_user(**extra_options)
defaults = { state: 'active', projects_limit: 0, email: "#{extra_options[:username]}@example.com" }
diff --git a/spec/migrations/20210915022415_cleanup_bigint_conversion_for_ci_builds_spec.rb b/spec/migrations/20210915022415_cleanup_bigint_conversion_for_ci_builds_spec.rb
index cedc62a6565..808c5371018 100644
--- a/spec/migrations/20210915022415_cleanup_bigint_conversion_for_ci_builds_spec.rb
+++ b/spec/migrations/20210915022415_cleanup_bigint_conversion_for_ci_builds_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupBigintConversionForCiBuilds do
+RSpec.describe CleanupBigintConversionForCiBuilds, feature_category: :continuous_integration do
let(:ci_builds) { table(:ci_builds) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20210918201050_remove_old_pending_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/migrations/20210918201050_remove_old_pending_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb
index d1c04c5d320..b3d1b41c330 100644
--- a/spec/migrations/20210918201050_remove_old_pending_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb
+++ b/spec/migrations/20210918201050_remove_old_pending_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb
@@ -21,10 +21,11 @@ def create_background_migration_jobs(ids, status, created_at)
)
end
-RSpec.describe RemoveOldPendingJobsForRecalculateVulnerabilitiesOccurrencesUuid, :migration do
- let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
- let_it_be(:before_target_date) { -Float::INFINITY..(DateTime.new(2021, 8, 17, 23, 59, 59)) }
- let_it_be(:after_target_date) { (DateTime.new(2021, 8, 18, 0, 0, 0))..Float::INFINITY }
+RSpec.describe RemoveOldPendingJobsForRecalculateVulnerabilitiesOccurrencesUuid, :migration,
+feature_category: :vulnerability_management do
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
+ let!(:before_target_date) { -Float::INFINITY..(DateTime.new(2021, 8, 17, 23, 59, 59)) }
+ let!(:after_target_date) { (DateTime.new(2021, 8, 18, 0, 0, 0))..Float::INFINITY }
context 'when old RecalculateVulnerabilitiesOccurrencesUuid jobs are pending' do
before do
diff --git a/spec/migrations/20210922021816_drop_int4_columns_for_ci_job_artifacts_spec.rb b/spec/migrations/20210922021816_drop_int4_columns_for_ci_job_artifacts_spec.rb
index a6eede8a8f1..c463f69c80c 100644
--- a/spec/migrations/20210922021816_drop_int4_columns_for_ci_job_artifacts_spec.rb
+++ b/spec/migrations/20210922021816_drop_int4_columns_for_ci_job_artifacts_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe DropInt4ColumnsForCiJobArtifacts do
+RSpec.describe DropInt4ColumnsForCiJobArtifacts, feature_category: :build_artifacts do
let(:ci_job_artifacts) { table(:ci_job_artifacts) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb b/spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb
index 730c9ade1fb..6b0c3a6db9a 100644
--- a/spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb
+++ b/spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe DropInt4ColumnForCiSourcesPipelines do
+RSpec.describe DropInt4ColumnForCiSourcesPipelines, feature_category: :pipeline_authoring do
let(:ci_sources_pipelines) { table(:ci_sources_pipelines) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20210922082019_drop_int4_column_for_events_spec.rb b/spec/migrations/20210922082019_drop_int4_column_for_events_spec.rb
index e460612a7d5..f615c8bb50e 100644
--- a/spec/migrations/20210922082019_drop_int4_column_for_events_spec.rb
+++ b/spec/migrations/20210922082019_drop_int4_column_for_events_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe DropInt4ColumnForEvents do
+RSpec.describe DropInt4ColumnForEvents, feature_category: :users do
let(:events) { table(:events) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20210922091402_drop_int4_column_for_push_event_payloads_spec.rb b/spec/migrations/20210922091402_drop_int4_column_for_push_event_payloads_spec.rb
index 8c89cd19f7f..5c39e7530ff 100644
--- a/spec/migrations/20210922091402_drop_int4_column_for_push_event_payloads_spec.rb
+++ b/spec/migrations/20210922091402_drop_int4_column_for_push_event_payloads_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe DropInt4ColumnForPushEventPayloads do
+RSpec.describe DropInt4ColumnForPushEventPayloads, feature_category: :users do
let(:push_event_payloads) { table(:push_event_payloads) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20211006060436_schedule_populate_topics_total_projects_count_cache_spec.rb b/spec/migrations/20211006060436_schedule_populate_topics_total_projects_count_cache_spec.rb
index 09ce0858b12..2f3903a20a9 100644
--- a/spec/migrations/20211006060436_schedule_populate_topics_total_projects_count_cache_spec.rb
+++ b/spec/migrations/20211006060436_schedule_populate_topics_total_projects_count_cache_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe SchedulePopulateTopicsTotalProjectsCountCache do
+RSpec.describe SchedulePopulateTopicsTotalProjectsCountCache, feature_category: :projects do
let(:topics) { table(:topics) }
let!(:topic_1) { topics.create!(name: 'Topic1') }
let!(:topic_2) { topics.create!(name: 'Topic2') }
diff --git a/spec/migrations/20211012134316_clean_up_migrate_merge_request_diff_commit_users_spec.rb b/spec/migrations/20211012134316_clean_up_migrate_merge_request_diff_commit_users_spec.rb
index 910e6d1d91b..f627ea825b3 100644
--- a/spec/migrations/20211012134316_clean_up_migrate_merge_request_diff_commit_users_spec.rb
+++ b/spec/migrations/20211012134316_clean_up_migrate_merge_request_diff_commit_users_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration! 'clean_up_migrate_merge_request_diff_commit_users'
-RSpec.describe CleanUpMigrateMergeRequestDiffCommitUsers, :migration do
+RSpec.describe CleanUpMigrateMergeRequestDiffCommitUsers, :migration, feature_category: :code_review do
describe '#up' do
context 'when there are pending jobs' do
it 'processes the jobs immediately' do
diff --git a/spec/migrations/20211018152654_schedule_remove_duplicate_vulnerabilities_findings3_spec.rb b/spec/migrations/20211018152654_schedule_remove_duplicate_vulnerabilities_findings3_spec.rb
index 95c5be2fc30..3e8176a36a1 100644
--- a/spec/migrations/20211018152654_schedule_remove_duplicate_vulnerabilities_findings3_spec.rb
+++ b/spec/migrations/20211018152654_schedule_remove_duplicate_vulnerabilities_findings3_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
require_migration!('schedule_remove_duplicate_vulnerabilities_findings3')
-RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings3, :migration do
+RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings3, :migration, feature_category: :vulnerability_management do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let(:users) { table(:users) }
let(:user) { create_user! }
@@ -88,7 +88,6 @@ RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings3, :migration do
let!(:unrelated_finding) do
create_finding!(
id: 9999999,
- uuid: "unreleated_finding",
vulnerability_id: nil,
report_type: 1,
location_fingerprint: 'random_location_fingerprint',
@@ -131,11 +130,10 @@ RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings3, :migration do
# rubocop:disable Metrics/ParameterLists
def create_finding!(
- id: nil,
- vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, id: nil,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
- metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
vulnerability_findings.create!({
id: id,
vulnerability_id: vulnerability_id,
diff --git a/spec/migrations/20211028155449_schedule_fix_merge_request_diff_commit_users_migration_spec.rb b/spec/migrations/20211028155449_schedule_fix_merge_request_diff_commit_users_migration_spec.rb
index 6511f554436..c7a0b938ca1 100644
--- a/spec/migrations/20211028155449_schedule_fix_merge_request_diff_commit_users_migration_spec.rb
+++ b/spec/migrations/20211028155449_schedule_fix_merge_request_diff_commit_users_migration_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration! 'schedule_fix_merge_request_diff_commit_users_migration'
-RSpec.describe ScheduleFixMergeRequestDiffCommitUsersMigration, :migration do
+RSpec.describe ScheduleFixMergeRequestDiffCommitUsersMigration, :migration, feature_category: :code_review do
let(:migration) { described_class.new }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/20211101222614_consume_remaining_user_namespace_jobs_spec.rb b/spec/migrations/20211101222614_consume_remaining_user_namespace_jobs_spec.rb
index d78ecc26ebf..1688ebf7cb1 100644
--- a/spec/migrations/20211101222614_consume_remaining_user_namespace_jobs_spec.rb
+++ b/spec/migrations/20211101222614_consume_remaining_user_namespace_jobs_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ConsumeRemainingUserNamespaceJobs do
+RSpec.describe ConsumeRemainingUserNamespaceJobs, feature_category: :subgroups do
let(:namespaces) { table(:namespaces) }
let!(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org', type: nil) }
diff --git a/spec/migrations/20211110143306_add_not_null_constraint_to_security_findings_uuid_spec.rb b/spec/migrations/20211110143306_add_not_null_constraint_to_security_findings_uuid_spec.rb
index 946fbf7f568..3b69169b2d6 100644
--- a/spec/migrations/20211110143306_add_not_null_constraint_to_security_findings_uuid_spec.rb
+++ b/spec/migrations/20211110143306_add_not_null_constraint_to_security_findings_uuid_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
require_migration!
-RSpec.describe AddNotNullConstraintToSecurityFindingsUuid do
- let_it_be(:security_findings) { table(:security_findings) }
- let_it_be(:migration) { described_class.new }
+RSpec.describe AddNotNullConstraintToSecurityFindingsUuid, feature_category: :vulnerability_management do
+ let!(:security_findings) { table(:security_findings) }
+ let!(:migration) { described_class.new }
before do
allow(migration).to receive(:transaction_open?).and_return(false)
diff --git a/spec/migrations/20211110151350_schedule_drop_invalid_security_findings_spec.rb b/spec/migrations/20211110151350_schedule_drop_invalid_security_findings_spec.rb
index b35cf5cbf4c..d05828112e6 100644
--- a/spec/migrations/20211110151350_schedule_drop_invalid_security_findings_spec.rb
+++ b/spec/migrations/20211110151350_schedule_drop_invalid_security_findings_spec.rb
@@ -3,20 +3,21 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleDropInvalidSecurityFindings, :migration, schema: 20211108211434 do
- let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
+RSpec.describe ScheduleDropInvalidSecurityFindings, :migration, :suppress_gitlab_schemas_validate_connection, schema: 20211108211434,
+ feature_category: :vulnerability_management do
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
- let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user', type: Namespaces::UserNamespace.sti_name) }
- let_it_be(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user', type: Namespaces::UserNamespace.sti_name) }
+ let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
- let_it_be(:pipelines) { table(:ci_pipelines) }
- let_it_be(:pipeline) { pipelines.create!(project_id: project.id) }
+ let!(:pipelines) { table(:ci_pipelines) }
+ let!(:pipeline) { pipelines.create!(project_id: project.id) }
- let_it_be(:ci_builds) { table(:ci_builds) }
- let_it_be(:ci_build) { ci_builds.create! }
+ let!(:ci_builds) { table(:ci_builds) }
+ let!(:ci_build) { ci_builds.create! }
- let_it_be(:security_scans) { table(:security_scans) }
- let_it_be(:security_scan) do
+ let!(:security_scans) { table(:security_scans) }
+ let!(:security_scan) do
security_scans.create!(
scan_type: 1,
status: 1,
@@ -26,11 +27,11 @@ RSpec.describe ScheduleDropInvalidSecurityFindings, :migration, schema: 20211108
)
end
- let_it_be(:vulnerability_scanners) { table(:vulnerability_scanners) }
- let_it_be(:vulnerability_scanner) { vulnerability_scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let!(:vulnerability_scanners) { table(:vulnerability_scanners) }
+ let!(:vulnerability_scanner) { vulnerability_scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
- let_it_be(:security_findings) { table(:security_findings) }
- let_it_be(:security_finding_without_uuid) do
+ let!(:security_findings) { table(:security_findings) }
+ let!(:security_finding_without_uuid) do
security_findings.create!(
severity: 1,
confidence: 1,
@@ -40,7 +41,7 @@ RSpec.describe ScheduleDropInvalidSecurityFindings, :migration, schema: 20211108
)
end
- let_it_be(:security_finding_with_uuid) do
+ let!(:security_finding_with_uuid) do
security_findings.create!(
severity: 1,
confidence: 1,
diff --git a/spec/migrations/20211116111644_schedule_remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb b/spec/migrations/20211116111644_schedule_remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb
index cf6a033b4b8..18513656029 100644
--- a/spec/migrations/20211116111644_schedule_remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb
+++ b/spec/migrations/20211116111644_schedule_remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings_spec.rb
@@ -4,23 +4,23 @@ require 'spec_helper'
require_migration!
RSpec.describe ScheduleRemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindings,
- :suppress_gitlab_schemas_validate_connection, :migration do
- let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
- let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
- let_it_be(:users) { table(:users) }
- let_it_be(:user) { create_user! }
- let_it_be(:project) { table(:projects).create!(id: 14219619, namespace_id: namespace.id) }
- let_it_be(:pipelines) { table(:ci_pipelines) }
- let_it_be(:scanners) { table(:vulnerability_scanners) }
- let_it_be(:scanner1) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
- let_it_be(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
- let_it_be(:scanner3) { scanners.create!(project_id: project.id, external_id: 'test 3', name: 'test scanner 3') }
- let_it_be(:unrelated_scanner) { scanners.create!(project_id: project.id, external_id: 'unreleated_scanner', name: 'unrelated scanner') }
- let_it_be(:vulnerabilities) { table(:vulnerabilities) }
- let_it_be(:vulnerability_findings) { table(:vulnerability_occurrences) }
- let_it_be(:vulnerability_finding_pipelines) { table(:vulnerability_occurrence_pipelines) }
- let_it_be(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
- let_it_be(:vulnerability_identifier) do
+ :suppress_gitlab_schemas_validate_connection, :migration, feature_category: :vulnerability_management do
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
+ let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let!(:users) { table(:users) }
+ let!(:user) { create_user! }
+ let!(:project) { table(:projects).create!(id: 14219619, namespace_id: namespace.id) }
+ let!(:pipelines) { table(:ci_pipelines) }
+ let!(:scanners) { table(:vulnerability_scanners) }
+ let!(:scanner1) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let!(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
+ let!(:scanner3) { scanners.create!(project_id: project.id, external_id: 'test 3', name: 'test scanner 3') }
+ let!(:unrelated_scanner) { scanners.create!(project_id: project.id, external_id: 'unreleated_scanner', name: 'unrelated scanner') }
+ let!(:vulnerabilities) { table(:vulnerabilities) }
+ let!(:vulnerability_findings) { table(:vulnerability_occurrences) }
+ let!(:vulnerability_finding_pipelines) { table(:vulnerability_occurrence_pipelines) }
+ let!(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+ let!(:vulnerability_identifier) do
vulnerability_identifiers.create!(
id: 1244459,
project_id: project.id,
@@ -30,14 +30,14 @@ RSpec.describe ScheduleRemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindi
name: 'vulnerability identifier')
end
- let_it_be(:vulnerability_for_first_duplicate) do
+ let!(:vulnerability_for_first_duplicate) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
- let_it_be(:first_finding_duplicate) do
+ let!(:first_finding_duplicate) do
create_finding!(
id: 5606961,
uuid: "bd95c085-71aa-51d7-9bb6-08ae669c262e",
@@ -50,14 +50,14 @@ RSpec.describe ScheduleRemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindi
)
end
- let_it_be(:vulnerability_for_second_duplicate) do
+ let!(:vulnerability_for_second_duplicate) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
- let_it_be(:second_finding_duplicate) do
+ let!(:second_finding_duplicate) do
create_finding!(
id: 8765432,
uuid: "5b714f58-1176-5b26-8fd5-e11dfcb031b5",
@@ -70,14 +70,14 @@ RSpec.describe ScheduleRemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindi
)
end
- let_it_be(:vulnerability_for_third_duplicate) do
+ let!(:vulnerability_for_third_duplicate) do
create_vulnerability!(
project_id: project.id,
author_id: user.id
)
end
- let_it_be(:third_finding_duplicate) do
+ let!(:third_finding_duplicate) do
create_finding!(
id: 8832995,
uuid: "cfe435fa-b25b-5199-a56d-7b007cc9e2d4",
@@ -90,10 +90,9 @@ RSpec.describe ScheduleRemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindi
)
end
- let_it_be(:unrelated_finding) do
+ let!(:unrelated_finding) do
create_finding!(
id: 9999999,
- uuid: "unreleated_finding",
vulnerability_id: nil,
report_type: 1,
location_fingerprint: 'random_location_fingerprint',
@@ -149,11 +148,10 @@ RSpec.describe ScheduleRemoveOccurrencePipelinesAndDuplicateVulnerabilitiesFindi
# rubocop:disable Metrics/ParameterLists
def create_finding!(
- id: nil,
- vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, id: nil,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
- metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
params = {
vulnerability_id: vulnerability_id,
project_id: project_id,
diff --git a/spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb b/spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb
index ef6dd94d9e3..bfe2b661a31 100644
--- a/spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb
+++ b/spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe MigrateRemainingU2fRegistrations, :migration do
+RSpec.describe MigrateRemainingU2fRegistrations, :migration, feature_category: :authentication_and_authorization do
let(:u2f_registrations) { table(:u2f_registrations) }
let(:webauthn_registrations) { table(:webauthn_registrations) }
let(:users) { table(:users) }
diff --git a/spec/migrations/20211126115449_encrypt_static_objects_external_storage_auth_token_spec.rb b/spec/migrations/20211126115449_encrypt_static_objects_external_storage_auth_token_spec.rb
index bf4094eaa49..09a8bb44d88 100644
--- a/spec/migrations/20211126115449_encrypt_static_objects_external_storage_auth_token_spec.rb
+++ b/spec/migrations/20211126115449_encrypt_static_objects_external_storage_auth_token_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe EncryptStaticObjectsExternalStorageAuthToken, :migration do
+RSpec.describe EncryptStaticObjectsExternalStorageAuthToken, :migration, feature_category: :source_code_management do
let(:application_settings) do
Class.new(ActiveRecord::Base) do
self.table_name = 'application_settings'
diff --git a/spec/migrations/20211126204445_add_task_to_work_item_types_spec.rb b/spec/migrations/20211126204445_add_task_to_work_item_types_spec.rb
index 34a6e2fdd12..32edd3615ff 100644
--- a/spec/migrations/20211126204445_add_task_to_work_item_types_spec.rb
+++ b/spec/migrations/20211126204445_add_task_to_work_item_types_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe AddTaskToWorkItemTypes, :migration do
+RSpec.describe AddTaskToWorkItemTypes, :migration, feature_category: :team_planning do
include MigrationHelpers::WorkItemTypesHelper
- let_it_be(:work_item_types) { table(:work_item_types) }
+ let!(:work_item_types) { table(:work_item_types) }
let(:base_types) do
{
diff --git a/spec/migrations/20211130165043_backfill_sequence_column_for_sprints_table_spec.rb b/spec/migrations/20211130165043_backfill_sequence_column_for_sprints_table_spec.rb
index 809ee53462f..91646da4791 100644
--- a/spec/migrations/20211130165043_backfill_sequence_column_for_sprints_table_spec.rb
+++ b/spec/migrations/20211130165043_backfill_sequence_column_for_sprints_table_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe BackfillSequenceColumnForSprintsTable, :migration, schema: 20211126042235 do
+RSpec.describe BackfillSequenceColumnForSprintsTable, :migration, schema: 20211126042235, feature_category: :team_planning do
let(:migration) { described_class.new }
let(:namespaces) { table(:namespaces) }
let(:sprints) { table(:sprints) }
diff --git a/spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb b/spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb
index 2e1289c58f7..7be54bc13cc 100644
--- a/spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb
+++ b/spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe AddIndexToProjectsOnMarkedForDeletionAt do
+RSpec.describe AddIndexToProjectsOnMarkedForDeletionAt, feature_category: :projects do
it 'correctly migrates up and down' do
reversible_migration do |migration|
migration.before -> {
diff --git a/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb
index 491aad1b30b..be89ee9d2aa 100644
--- a/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb
+++ b/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb
@@ -20,8 +20,9 @@ def create_background_migration_jobs(ids, status, created_at)
)
end
-RSpec.describe RemoveJobsForRecalculateVulnerabilitiesOccurrencesUuid, :migration do
- let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
+RSpec.describe RemoveJobsForRecalculateVulnerabilitiesOccurrencesUuid, :migration,
+feature_category: :vulnerability_management do
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
context 'when RecalculateVulnerabilitiesOccurrencesUuid jobs are present' do
before do
diff --git a/spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb b/spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb
index 71ffcafaae1..c7401c4790d 100644
--- a/spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb
+++ b/spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences4 do
+RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences4, feature_category: :vulnerability_management do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let(:users) { table(:users) }
let(:user) { create_user! }
diff --git a/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb b/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb
index 289cf9a93ed..f103ee54990 100644
--- a/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb
+++ b/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb
@@ -3,9 +3,9 @@ require 'spec_helper'
require_migration!
-RSpec.describe EncryptStaticObjectToken, :migration do
- let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
- let_it_be(:users) { table(:users) }
+RSpec.describe EncryptStaticObjectToken, :migration, feature_category: :source_code_management do
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
+ let!(:users) { table(:users) }
let!(:user_without_tokens) { create_user!(name: 'notoken') }
let!(:user_with_plaintext_token_1) { create_user!(name: 'plaintext_1', token: 'token') }
diff --git a/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb b/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb
index 791c0595f0e..0df52df43d8 100644
--- a/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb
+++ b/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillIncidentIssueEscalationStatuses do
+RSpec.describe BackfillIncidentIssueEscalationStatuses, feature_category: :incident_management do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:issues) { table(:issues) }
diff --git a/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb
index c5058f30d82..2d808adf578 100644
--- a/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb
+++ b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb
@@ -20,8 +20,8 @@ def create_background_migration_jobs(ids, status, created_at)
)
end
-RSpec.describe MarkRecalculateFindingSignaturesAsCompleted, :migration do
- let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
+RSpec.describe MarkRecalculateFindingSignaturesAsCompleted, :migration, feature_category: :vulnerability_management do
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
context 'when RecalculateVulnerabilitiesOccurrencesUuid jobs are present' do
before do
diff --git a/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb
index 3e450546315..263289462ba 100644
--- a/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb
+++ b/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddInsertOrUpdateVulnerabilityReadsTrigger do
+RSpec.describe AddInsertOrUpdateVulnerabilityReadsTrigger, feature_category: :vulnerability_management do
let(:migration) { described_class.new }
let(:vulnerabilities) { table(:vulnerabilities) }
let(:vulnerability_reads) { table(:vulnerability_reads) }
@@ -126,7 +126,7 @@ RSpec.describe AddInsertOrUpdateVulnerabilityReadsTrigger do
# rubocop:disable Metrics/ParameterLists
def create_finding!(
- vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
diff --git a/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb
index d988b1e42b9..152a551bc7b 100644
--- a/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb
+++ b/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddUpdateVulnerabilityReadsTrigger do
+RSpec.describe AddUpdateVulnerabilityReadsTrigger, feature_category: :vulnerability_management do
let(:migration) { described_class.new }
let(:vulnerability_reads) { table(:vulnerability_reads) }
let(:issue_links) { table(:vulnerability_issue_links) }
@@ -103,7 +103,7 @@ RSpec.describe AddUpdateVulnerabilityReadsTrigger do
# rubocop:disable Metrics/ParameterLists
def create_finding!(
- vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
diff --git a/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb b/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb
index 901f1cf6041..9fc40b0b5f1 100644
--- a/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb
+++ b/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddUpdateVulnerabilityReadsLocationTrigger do
+RSpec.describe AddUpdateVulnerabilityReadsLocationTrigger, feature_category: :vulnerability_management do
let(:migration) { described_class.new }
let(:vulnerability_reads) { table(:vulnerability_reads) }
let(:issue_links) { table(:vulnerability_issue_links) }
@@ -111,7 +111,7 @@ RSpec.describe AddUpdateVulnerabilityReadsLocationTrigger do
# rubocop:disable Metrics/ParameterLists
def create_finding!(
- vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
diff --git a/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb
index 8e50b74eb9c..e58fdfb1591 100644
--- a/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb
+++ b/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddHasIssuesOnVulnerabilityReadsTrigger do
+RSpec.describe AddHasIssuesOnVulnerabilityReadsTrigger, feature_category: :vulnerability_management do
let(:migration) { described_class.new }
let(:vulnerability_reads) { table(:vulnerability_reads) }
let(:issue_links) { table(:vulnerability_issue_links) }
@@ -109,7 +109,7 @@ RSpec.describe AddHasIssuesOnVulnerabilityReadsTrigger do
# rubocop:disable Metrics/ParameterLists
def create_finding!(
- vulnerability_id: nil, project_id:, scanner_id:, primary_identifier_id:,
+ project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
diff --git a/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb b/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb
index 063a51227dd..1338f826537 100644
--- a/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb
+++ b/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb
@@ -3,17 +3,17 @@ require 'spec_helper'
require_migration!
-RSpec.describe PopulateVulnerabilityReads, :migration do
- let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
- let_it_be(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) }
- let_it_be(:project) { table(:projects).create!(namespace_id: namespace.id) }
- let_it_be(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
- let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
- let_it_be(:vulnerabilities) { table(:vulnerabilities) }
- let_it_be(:vulnerability_reads) { table(:vulnerability_reads) }
- let_it_be(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
- let_it_be(:vulnerability_issue_links) { table(:vulnerability_issue_links) }
- let_it_be(:vulnerability_ids) { [] }
+RSpec.describe PopulateVulnerabilityReads, :migration, feature_category: :vulnerability_management do
+ let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let!(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
+ let!(:vulnerabilities) { table(:vulnerabilities) }
+ let!(:vulnerability_reads) { table(:vulnerability_reads) }
+ let!(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let!(:vulnerability_issue_links) { table(:vulnerability_issue_links) }
+ let!(:vulnerability_ids) { [] }
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
@@ -80,8 +80,7 @@ RSpec.describe PopulateVulnerabilityReads, :migration do
# rubocop:disable Metrics/ParameterLists
def create_finding!(
- id: nil,
- vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, id: nil,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
diff --git a/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb b/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb
index 2ad9a8220c3..1470f2b3cad 100644
--- a/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb
+++ b/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!('drop_position_from_security_findings')
-RSpec.describe DropPositionFromSecurityFindings do
+RSpec.describe DropPositionFromSecurityFindings, feature_category: :vulnerability_management do
let(:events) { table(:security_findings) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20220124130028_dedup_runner_projects_spec.rb b/spec/migrations/20220124130028_dedup_runner_projects_spec.rb
index 3429ccc4df1..ee468f40908 100644
--- a/spec/migrations/20220124130028_dedup_runner_projects_spec.rb
+++ b/spec/migrations/20220124130028_dedup_runner_projects_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe DedupRunnerProjects, :migration, :suppress_gitlab_schemas_validate_connection, schema: 20220120085655 do
+RSpec.describe DedupRunnerProjects, :migration, :suppress_gitlab_schemas_validate_connection,
+schema: 20220120085655, feature_category: :runner do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:runners) { table(:ci_runners) }
diff --git a/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb b/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb
index a23f9995875..ea88cf1a2ce 100644
--- a/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb
+++ b/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!('remove_dangling_running_builds')
-RSpec.describe RemoveDanglingRunningBuilds, :suppress_gitlab_schemas_validate_connection do
+RSpec.describe RemoveDanglingRunningBuilds, :suppress_gitlab_schemas_validate_connection,
+feature_category: :continuous_integration do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let(:project) { table(:projects).create!(namespace_id: namespace.id) }
let(:runner) { table(:ci_runners).create!(runner_type: 1) }
diff --git a/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb b/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb
index 1558facdf96..3f3fdd0889d 100644
--- a/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb
+++ b/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!('fix_approval_rules_code_owners_rule_type_index')
-RSpec.describe FixApprovalRulesCodeOwnersRuleTypeIndex, :migration do
+RSpec.describe FixApprovalRulesCodeOwnersRuleTypeIndex, :migration, feature_category: :source_code_management do
let(:table_name) { :approval_merge_request_rules }
let(:index_name) { 'index_approval_rules_code_owners_rule_type' }
diff --git a/spec/migrations/20220202105733_delete_service_template_records_spec.rb b/spec/migrations/20220202105733_delete_service_template_records_spec.rb
index c9f6b5cbe66..41762a3a5c3 100644
--- a/spec/migrations/20220202105733_delete_service_template_records_spec.rb
+++ b/spec/migrations/20220202105733_delete_service_template_records_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe DeleteServiceTemplateRecords do
+RSpec.describe DeleteServiceTemplateRecords, feature_category: :integrations do
let(:integrations) { table(:integrations) }
let(:chat_names) { table(:chat_names) }
let(:web_hooks) { table(:web_hooks) }
diff --git a/spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb b/spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb
index 39398fa058d..cbae5674d78 100644
--- a/spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb
+++ b/spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb
@@ -3,23 +3,23 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillNamespaceStatisticsWithDependencyProxySize do
- let_it_be(:groups) { table(:namespaces) }
- let_it_be(:group1) { groups.create!(id: 10, name: 'test1', path: 'test1', type: 'Group') }
- let_it_be(:group2) { groups.create!(id: 20, name: 'test2', path: 'test2', type: 'Group') }
- let_it_be(:group3) { groups.create!(id: 30, name: 'test3', path: 'test3', type: 'Group') }
- let_it_be(:group4) { groups.create!(id: 40, name: 'test4', path: 'test4', type: 'Group') }
+RSpec.describe BackfillNamespaceStatisticsWithDependencyProxySize, feature_category: :dependency_proxy do
+ let!(:groups) { table(:namespaces) }
+ let!(:group1) { groups.create!(id: 10, name: 'test1', path: 'test1', type: 'Group') }
+ let!(:group2) { groups.create!(id: 20, name: 'test2', path: 'test2', type: 'Group') }
+ let!(:group3) { groups.create!(id: 30, name: 'test3', path: 'test3', type: 'Group') }
+ let!(:group4) { groups.create!(id: 40, name: 'test4', path: 'test4', type: 'Group') }
- let_it_be(:dependency_proxy_blobs) { table(:dependency_proxy_blobs) }
- let_it_be(:dependency_proxy_manifests) { table(:dependency_proxy_manifests) }
+ let!(:dependency_proxy_blobs) { table(:dependency_proxy_blobs) }
+ let!(:dependency_proxy_manifests) { table(:dependency_proxy_manifests) }
- let_it_be(:group1_manifest) { create_manifest(10, 10) }
- let_it_be(:group2_manifest) { create_manifest(20, 20) }
- let_it_be(:group3_manifest) { create_manifest(30, 30) }
+ let!(:group1_manifest) { create_manifest(10, 10) }
+ let!(:group2_manifest) { create_manifest(20, 20) }
+ let!(:group3_manifest) { create_manifest(30, 30) }
- let_it_be(:group1_blob) { create_blob(10, 10) }
- let_it_be(:group2_blob) { create_blob(20, 20) }
- let_it_be(:group3_blob) { create_blob(30, 30) }
+ let!(:group1_blob) { create_blob(10, 10) }
+ let!(:group2_blob) { create_blob(20, 20) }
+ let!(:group3_blob) { create_blob(30, 30) }
describe '#up' do
it 'correctly schedules background migrations' do
diff --git a/spec/migrations/20220204194347_encrypt_integration_properties_spec.rb b/spec/migrations/20220204194347_encrypt_integration_properties_spec.rb
index 78e3b43ff76..5e728bb396c 100644
--- a/spec/migrations/20220204194347_encrypt_integration_properties_spec.rb
+++ b/spec/migrations/20220204194347_encrypt_integration_properties_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe EncryptIntegrationProperties, :migration, schema: 20220204193000 do
+RSpec.describe EncryptIntegrationProperties, :migration, schema: 20220204193000, feature_category: :integrations do
subject(:migration) { described_class.new }
let(:integrations) { table(:integrations) }
diff --git a/spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb b/spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb
index 41f3476dea8..89583d1050b 100644
--- a/spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb
+++ b/spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleMigratePersonalNamespaceProjectMaintainerToOwner do
- let_it_be(:migration) { described_class::MIGRATION }
+RSpec.describe ScheduleMigratePersonalNamespaceProjectMaintainerToOwner, feature_category: :subgroups do
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of members' do
diff --git a/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb b/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb
index bf79ee02ff1..8a6a542bc5e 100644
--- a/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb
+++ b/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe UpdateIntegrationsTriggerTypeNewOnInsertNullSafe, :migration do
+RSpec.describe UpdateIntegrationsTriggerTypeNewOnInsertNullSafe, :migration, feature_category: :integrations do
let(:integrations) { table(:integrations) }
before do
diff --git a/spec/migrations/20220213103859_remove_integrations_type_spec.rb b/spec/migrations/20220213103859_remove_integrations_type_spec.rb
index b1a4370700a..8f6d9b0d9b5 100644
--- a/spec/migrations/20220213103859_remove_integrations_type_spec.rb
+++ b/spec/migrations/20220213103859_remove_integrations_type_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe RemoveIntegrationsType, :migration do
+RSpec.describe RemoveIntegrationsType, :migration, feature_category: :integrations do
subject(:migration) { described_class.new }
let(:integrations) { table(:integrations) }
diff --git a/spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb b/spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb
index bd7d992240a..b8a37dcd6d9 100644
--- a/spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb
+++ b/spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
require_migration!
-RSpec.describe CreateNotNullConstraintReleasesTag do
- let_it_be(:releases) { table(:releases) }
- let_it_be(:migration) { described_class.new }
+RSpec.describe CreateNotNullConstraintReleasesTag, feature_category: :release_orchestration do
+ let!(:releases) { table(:releases) }
+ let!(:migration) { described_class.new }
before do
allow(migration).to receive(:transaction_open?).and_return(false)
diff --git a/spec/migrations/20220222192525_remove_null_releases_spec.rb b/spec/migrations/20220222192525_remove_null_releases_spec.rb
index 6043f2c8cc8..ce42dea077d 100644
--- a/spec/migrations/20220222192525_remove_null_releases_spec.rb
+++ b/spec/migrations/20220222192525_remove_null_releases_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe RemoveNullReleases do
+RSpec.describe RemoveNullReleases, feature_category: :release_orchestration do
let(:releases) { table(:releases) }
before do
diff --git a/spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb b/spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb
index d9f6729475c..425f622581b 100644
--- a/spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb
+++ b/spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleMergeTopicsWithSameName do
+RSpec.describe ScheduleMergeTopicsWithSameName, feature_category: :projects do
let(:topics) { table(:topics) }
describe '#up' do
diff --git a/spec/migrations/20220305223212_add_security_training_providers_spec.rb b/spec/migrations/20220305223212_add_security_training_providers_spec.rb
index 3d0089aaa8d..f67db3b68cd 100644
--- a/spec/migrations/20220305223212_add_security_training_providers_spec.rb
+++ b/spec/migrations/20220305223212_add_security_training_providers_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe AddSecurityTrainingProviders, :migration do
+RSpec.describe AddSecurityTrainingProviders, :migration, feature_category: :vulnerability_management do
include MigrationHelpers::WorkItemTypesHelper
- let_it_be(:security_training_providers) { table(:security_training_providers) }
+ let!(:security_training_providers) { table(:security_training_providers) }
it 'creates default data' do
# Need to delete all as security training providers are seeded before entire test suite
diff --git a/spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb b/spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb
index 8a653869a9b..3bdd6e5fab9 100644
--- a/spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb
+++ b/spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe RemoveDuplicateProjectTagReleases do
+RSpec.describe RemoveDuplicateProjectTagReleases, feature_category: :release_orchestration do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:users) { table(:users) }
diff --git a/spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb b/spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb
index c471fd86bf5..a57d3633ecf 100644
--- a/spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb
+++ b/spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe RemoveLeftoverExternalPullRequestDeletions do
+RSpec.describe RemoveLeftoverExternalPullRequestDeletions, feature_category: :sharding do
let(:deleted_records) { table(:loose_foreign_keys_deleted_records) }
let(:pending_record1) { deleted_records.create!(id: 1, fully_qualified_table_name: 'public.external_pull_requests', primary_key_value: 1, status: 1) }
diff --git a/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb b/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb
index c00685c1397..f40f9c70833 100644
--- a/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb
+++ b/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe RemoveDependencyListUsageDataFromRedis, :migration, :clean_gitlab_redis_shared_state do
+RSpec.describe RemoveDependencyListUsageDataFromRedis, :migration, :clean_gitlab_redis_shared_state,
+feature_category: :dependency_management do
let(:key) { "DEPENDENCY_LIST_USAGE_COUNTER" }
describe "#up" do
diff --git a/spec/migrations/20220315171129_cleanup_draft_data_from_faulty_regex_spec.rb b/spec/migrations/20220315171129_cleanup_draft_data_from_faulty_regex_spec.rb
index 925f1e573be..1760535e66f 100644
--- a/spec/migrations/20220315171129_cleanup_draft_data_from_faulty_regex_spec.rb
+++ b/spec/migrations/20220315171129_cleanup_draft_data_from_faulty_regex_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupDraftDataFromFaultyRegex do
+RSpec.describe CleanupDraftDataFromFaultyRegex, feature_category: :code_review do
let(:merge_requests) { table(:merge_requests) }
let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
diff --git a/spec/migrations/20220316202640_populate_container_repositories_migration_plan_spec.rb b/spec/migrations/20220316202640_populate_container_repositories_migration_plan_spec.rb
index 7b5c8254163..16ebbf8b004 100644
--- a/spec/migrations/20220316202640_populate_container_repositories_migration_plan_spec.rb
+++ b/spec/migrations/20220316202640_populate_container_repositories_migration_plan_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe PopulateContainerRepositoriesMigrationPlan, :aggregate_failures do
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:projects) { table(:projects) }
- let_it_be(:container_repositories) { table(:container_repositories) }
+RSpec.describe PopulateContainerRepositoriesMigrationPlan, :aggregate_failures, feature_category: :container_registry do
+ let!(:namespaces) { table(:namespaces) }
+ let!(:projects) { table(:projects) }
+ let!(:container_repositories) { table(:container_repositories) }
let!(:namespace) { namespaces.create!(id: 1, name: 'namespace', path: 'namespace') }
let!(:project) { projects.create!(id: 1, name: 'project', path: 'project', namespace_id: 1) }
diff --git a/spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb b/spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb
index 44e20df1130..c645a768969 100644
--- a/spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb
+++ b/spec/migrations/20220321234317_remove_all_issuable_escalation_statuses_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe RemoveAllIssuableEscalationStatuses do
+RSpec.describe RemoveAllIssuableEscalationStatuses, feature_category: :incident_management do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:issues) { table(:issues) }
diff --git a/spec/migrations/20220322132242_update_pages_onboarding_state_spec.rb b/spec/migrations/20220322132242_update_pages_onboarding_state_spec.rb
index fbd5fe546fa..6b08b4f853d 100644
--- a/spec/migrations/20220322132242_update_pages_onboarding_state_spec.rb
+++ b/spec/migrations/20220322132242_update_pages_onboarding_state_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe UpdatePagesOnboardingState do
+RSpec.describe UpdatePagesOnboardingState, feature_category: :pages do
let(:migration) { described_class.new }
let!(:namespaces) { table(:namespaces) }
let!(:projects) { table(:projects) }
diff --git a/spec/migrations/20220324032250_migrate_shimo_confluence_service_category_spec.rb b/spec/migrations/20220324032250_migrate_shimo_confluence_service_category_spec.rb
index 38db6d51e7e..15c16a2b232 100644
--- a/spec/migrations/20220324032250_migrate_shimo_confluence_service_category_spec.rb
+++ b/spec/migrations/20220324032250_migrate_shimo_confluence_service_category_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe MigrateShimoConfluenceServiceCategory, :migration do
+RSpec.describe MigrateShimoConfluenceServiceCategory, :migration, feature_category: :integrations do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:integrations) { table(:integrations) }
diff --git a/spec/migrations/20220324165436_schedule_backfill_project_settings_spec.rb b/spec/migrations/20220324165436_schedule_backfill_project_settings_spec.rb
index a8014e73bf0..3fcfb84c214 100644
--- a/spec/migrations/20220324165436_schedule_backfill_project_settings_spec.rb
+++ b/spec/migrations/20220324165436_schedule_backfill_project_settings_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleBackfillProjectSettings do
- let_it_be(:migration) { described_class::MIGRATION }
+RSpec.describe ScheduleBackfillProjectSettings, feature_category: :projects do
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of projects' do
diff --git a/spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb b/spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb
index 13884007af2..555856788b7 100644
--- a/spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb
+++ b/spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe RemoveLeftoverCiJobArtifactDeletions do
+RSpec.describe RemoveLeftoverCiJobArtifactDeletions, feature_category: :sharding do
let(:deleted_records) { table(:loose_foreign_keys_deleted_records) }
target_table_name = Ci::JobArtifact.table_name
diff --git a/spec/migrations/20220331133802_schedule_backfill_topics_title_spec.rb b/spec/migrations/20220331133802_schedule_backfill_topics_title_spec.rb
index 13e8c42269b..b26cd9688ae 100644
--- a/spec/migrations/20220331133802_schedule_backfill_topics_title_spec.rb
+++ b/spec/migrations/20220331133802_schedule_backfill_topics_title_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleBackfillTopicsTitle do
+RSpec.describe ScheduleBackfillTopicsTitle, feature_category: :projects do
let(:topics) { table(:topics) }
let!(:topic1) { topics.create!(name: 'topic1') }
diff --git a/spec/migrations/20220412143552_consume_remaining_encrypt_integration_property_jobs_spec.rb b/spec/migrations/20220412143552_consume_remaining_encrypt_integration_property_jobs_spec.rb
index 4a1b68a5a85..77bf80621c4 100644
--- a/spec/migrations/20220412143552_consume_remaining_encrypt_integration_property_jobs_spec.rb
+++ b/spec/migrations/20220412143552_consume_remaining_encrypt_integration_property_jobs_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe ConsumeRemainingEncryptIntegrationPropertyJobs, :migration do
+RSpec.describe ConsumeRemainingEncryptIntegrationPropertyJobs, :migration, feature_category: :integrations do
subject(:migration) { described_class.new }
let(:integrations) { table(:integrations) }
diff --git a/spec/migrations/20220416054011_schedule_backfill_project_member_namespace_id_spec.rb b/spec/migrations/20220416054011_schedule_backfill_project_member_namespace_id_spec.rb
index 2838fc9387c..c81ecc07779 100644
--- a/spec/migrations/20220416054011_schedule_backfill_project_member_namespace_id_spec.rb
+++ b/spec/migrations/20220416054011_schedule_backfill_project_member_namespace_id_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleBackfillProjectMemberNamespaceId do
- let_it_be(:migration) { described_class::MIGRATION }
+RSpec.describe ScheduleBackfillProjectMemberNamespaceId, feature_category: :subgroups do
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of project members' do
diff --git a/spec/migrations/20220420135946_update_batched_background_migration_arguments_spec.rb b/spec/migrations/20220420135946_update_batched_background_migration_arguments_spec.rb
index 6dbee483e15..c740c893ad6 100644
--- a/spec/migrations/20220420135946_update_batched_background_migration_arguments_spec.rb
+++ b/spec/migrations/20220420135946_update_batched_background_migration_arguments_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe UpdateBatchedBackgroundMigrationArguments do
+RSpec.describe UpdateBatchedBackgroundMigrationArguments, feature_category: :database do
let(:batched_migrations) { table(:batched_background_migrations) }
before do
diff --git a/spec/migrations/20220426185933_backfill_deployments_finished_at_spec.rb b/spec/migrations/20220426185933_backfill_deployments_finished_at_spec.rb
index c79325c5077..c41e1402bf1 100644
--- a/spec/migrations/20220426185933_backfill_deployments_finished_at_spec.rb
+++ b/spec/migrations/20220426185933_backfill_deployments_finished_at_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe BackfillDeploymentsFinishedAt, :migration do
+RSpec.describe BackfillDeploymentsFinishedAt, :migration, feature_category: :continuous_delivery do
let(:deployments) { table(:deployments) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20220502015011_clean_up_fix_merge_request_diff_commit_users_spec.rb b/spec/migrations/20220502015011_clean_up_fix_merge_request_diff_commit_users_spec.rb
index 2bc3e89a748..e316ad25214 100644
--- a/spec/migrations/20220502015011_clean_up_fix_merge_request_diff_commit_users_spec.rb
+++ b/spec/migrations/20220502015011_clean_up_fix_merge_request_diff_commit_users_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration! 'clean_up_fix_merge_request_diff_commit_users'
-RSpec.describe CleanUpFixMergeRequestDiffCommitUsers, :migration do
+RSpec.describe CleanUpFixMergeRequestDiffCommitUsers, :migration, feature_category: :code_review do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:project_namespace) { namespaces.create!(name: 'project2', path: 'project2', type: 'Project') }
diff --git a/spec/migrations/20220502173045_reset_too_many_tags_skipped_registry_imports_spec.rb b/spec/migrations/20220502173045_reset_too_many_tags_skipped_registry_imports_spec.rb
index cc4041fe151..a65e991d566 100644
--- a/spec/migrations/20220502173045_reset_too_many_tags_skipped_registry_imports_spec.rb
+++ b/spec/migrations/20220502173045_reset_too_many_tags_skipped_registry_imports_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ResetTooManyTagsSkippedRegistryImports, :aggregate_failures do
+RSpec.describe ResetTooManyTagsSkippedRegistryImports, :aggregate_failures, feature_category: :container_registry do
let(:migration) { described_class::MIGRATION }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/20220503035221_add_gitlab_schema_to_batched_background_migrations_spec.rb b/spec/migrations/20220503035221_add_gitlab_schema_to_batched_background_migrations_spec.rb
index 5002c665c79..9086700c513 100644
--- a/spec/migrations/20220503035221_add_gitlab_schema_to_batched_background_migrations_spec.rb
+++ b/spec/migrations/20220503035221_add_gitlab_schema_to_batched_background_migrations_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe AddGitlabSchemaToBatchedBackgroundMigrations do
+RSpec.describe AddGitlabSchemaToBatchedBackgroundMigrations, feature_category: :database do
it 'sets gitlab_schema for existing methods to "gitlab_main" and default to NULL' do
batched_migrations = table(:batched_background_migrations)
batched_migration = batched_migrations.create!(
diff --git a/spec/migrations/20220505044348_fix_automatic_iterations_cadences_start_date_spec.rb b/spec/migrations/20220505044348_fix_automatic_iterations_cadences_start_date_spec.rb
index 575157f8331..3a6a8f5dbe5 100644
--- a/spec/migrations/20220505044348_fix_automatic_iterations_cadences_start_date_spec.rb
+++ b/spec/migrations/20220505044348_fix_automatic_iterations_cadences_start_date_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe FixAutomaticIterationsCadencesStartDate do
+RSpec.describe FixAutomaticIterationsCadencesStartDate, feature_category: :team_planning do
let(:migration) { described_class.new }
let(:namespaces) { table(:namespaces) }
let(:sprints) { table(:sprints) }
diff --git a/spec/migrations/20220505174658_update_index_on_alerts_to_exclude_null_fingerprints_spec.rb b/spec/migrations/20220505174658_update_index_on_alerts_to_exclude_null_fingerprints_spec.rb
index ec58a54b085..255d99eb8ca 100644
--- a/spec/migrations/20220505174658_update_index_on_alerts_to_exclude_null_fingerprints_spec.rb
+++ b/spec/migrations/20220505174658_update_index_on_alerts_to_exclude_null_fingerprints_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe UpdateIndexOnAlertsToExcludeNullFingerprints do
+RSpec.describe UpdateIndexOnAlertsToExcludeNullFingerprints, feature_category: :incident_management do
let(:alerts) { 'alert_management_alerts' }
let(:old_index) { described_class::OLD_INDEX_NAME }
let(:new_index) { described_class::NEW_INDEX_NAME }
diff --git a/spec/migrations/20220506154054_create_sync_namespace_details_trigger_spec.rb b/spec/migrations/20220506154054_create_sync_namespace_details_trigger_spec.rb
index 411b1eacb86..3e784761dd4 100644
--- a/spec/migrations/20220506154054_create_sync_namespace_details_trigger_spec.rb
+++ b/spec/migrations/20220506154054_create_sync_namespace_details_trigger_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe CreateSyncNamespaceDetailsTrigger do
+RSpec.describe CreateSyncNamespaceDetailsTrigger, feature_category: :subgroups do
let(:migration) { described_class.new }
let(:namespaces) { table(:namespaces) }
let(:namespace_details) { table(:namespace_details) }
diff --git a/spec/migrations/20220512190659_remove_web_hooks_web_hook_logs_web_hook_id_fk_spec.rb b/spec/migrations/20220512190659_remove_web_hooks_web_hook_logs_web_hook_id_fk_spec.rb
index fa94a73582d..66649eebf70 100644
--- a/spec/migrations/20220512190659_remove_web_hooks_web_hook_logs_web_hook_id_fk_spec.rb
+++ b/spec/migrations/20220512190659_remove_web_hooks_web_hook_logs_web_hook_id_fk_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe RemoveWebHooksWebHookLogsWebHookIdFk do
+RSpec.describe RemoveWebHooksWebHookLogsWebHookIdFk, feature_category: :integrations do
let(:web_hooks) { table(:web_hooks) }
let(:logs) { table(:web_hook_logs) }
diff --git a/spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb b/spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb
index 63fff279acc..735232dfac7 100644
--- a/spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb
+++ b/spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe RescheduleExpireOAuthTokens do
- let_it_be(:migration) { described_class::MIGRATION }
+RSpec.describe RescheduleExpireOAuthTokens, feature_category: :authentication_and_authorization do
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of oauth tokens' do
diff --git a/spec/migrations/20220523171107_drop_deploy_tokens_token_column_spec.rb b/spec/migrations/20220523171107_drop_deploy_tokens_token_column_spec.rb
index 78df6f5fc35..9cbc6dea6a9 100644
--- a/spec/migrations/20220523171107_drop_deploy_tokens_token_column_spec.rb
+++ b/spec/migrations/20220523171107_drop_deploy_tokens_token_column_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe DropDeployTokensTokenColumn do
+RSpec.describe DropDeployTokensTokenColumn, feature_category: :continuous_delivery do
let(:deploy_tokens) { table(:deploy_tokens) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20220524074947_finalize_backfill_null_note_discussion_ids_spec.rb b/spec/migrations/20220524074947_finalize_backfill_null_note_discussion_ids_spec.rb
index 74ad4662b3e..9071c61ca0e 100644
--- a/spec/migrations/20220524074947_finalize_backfill_null_note_discussion_ids_spec.rb
+++ b/spec/migrations/20220524074947_finalize_backfill_null_note_discussion_ids_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe FinalizeBackfillNullNoteDiscussionIds, :migration do
+RSpec.describe FinalizeBackfillNullNoteDiscussionIds, :migration, feature_category: :team_planning do
subject(:migration) { described_class.new }
let(:notes) { table(:notes) }
diff --git a/spec/migrations/20220524184149_create_sync_project_namespace_details_trigger_spec.rb b/spec/migrations/20220524184149_create_sync_project_namespace_details_trigger_spec.rb
index f85a59357e1..21fddb08771 100644
--- a/spec/migrations/20220524184149_create_sync_project_namespace_details_trigger_spec.rb
+++ b/spec/migrations/20220524184149_create_sync_project_namespace_details_trigger_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe CreateSyncProjectNamespaceDetailsTrigger do
+RSpec.describe CreateSyncProjectNamespaceDetailsTrigger, feature_category: :projects do
let(:migration) { described_class.new }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20220525221133_schedule_backfill_vulnerability_reads_cluster_agent_spec.rb b/spec/migrations/20220525221133_schedule_backfill_vulnerability_reads_cluster_agent_spec.rb
index 3f1a2d8c4b9..9e414157b3f 100644
--- a/spec/migrations/20220525221133_schedule_backfill_vulnerability_reads_cluster_agent_spec.rb
+++ b/spec/migrations/20220525221133_schedule_backfill_vulnerability_reads_cluster_agent_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleBackfillVulnerabilityReadsClusterAgent do
- let_it_be(:batched_migration) { described_class::MIGRATION_NAME }
+RSpec.describe ScheduleBackfillVulnerabilityReadsClusterAgent, feature_category: :vulnerability_management do
+ let!(:batched_migration) { described_class::MIGRATION_NAME }
it 'schedules background jobs for each batch of vulnerability reads' do
reversible_migration do |migration|
diff --git a/spec/migrations/20220601110011_schedule_remove_self_managed_wiki_notes_spec.rb b/spec/migrations/20220601110011_schedule_remove_self_managed_wiki_notes_spec.rb
index 44e80980b27..63174d054d7 100644
--- a/spec/migrations/20220601110011_schedule_remove_self_managed_wiki_notes_spec.rb
+++ b/spec/migrations/20220601110011_schedule_remove_self_managed_wiki_notes_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleRemoveSelfManagedWikiNotes do
- let_it_be(:batched_migration) { described_class::MIGRATION }
+RSpec.describe ScheduleRemoveSelfManagedWikiNotes, feature_category: :wiki do
+ let!(:batched_migration) { described_class::MIGRATION }
it 'schedules new batched migration' do
reversible_migration do |migration|
diff --git a/spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb b/spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb
index 8cb6ab23fef..1b8ec47f61b 100644
--- a/spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb
+++ b/spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe AddUserIdAndIpAddressSuccessIndexToAuthenticationEvents do
+RSpec.describe AddUserIdAndIpAddressSuccessIndexToAuthenticationEvents,
+feature_category: :authentication_and_authorization do
let(:db) { described_class.new }
let(:old_index) { described_class::OLD_INDEX_NAME }
let(:new_index) { described_class::NEW_INDEX_NAME }
diff --git a/spec/migrations/20220606080509_fix_incorrect_job_artifacts_expire_at_spec.rb b/spec/migrations/20220606080509_fix_incorrect_job_artifacts_expire_at_spec.rb
index 5921dd64c0e..314385e35da 100644
--- a/spec/migrations/20220606080509_fix_incorrect_job_artifacts_expire_at_spec.rb
+++ b/spec/migrations/20220606080509_fix_incorrect_job_artifacts_expire_at_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe FixIncorrectJobArtifactsExpireAt, migration: :gitlab_ci do
- let_it_be(:batched_migration) { described_class::MIGRATION }
+RSpec.describe FixIncorrectJobArtifactsExpireAt, migration: :gitlab_ci, feature_category: :build_artifacts do
+ let!(:batched_migration) { described_class::MIGRATION }
it 'does not schedule background jobs when Gitlab.com is true' do
allow(Gitlab).to receive(:com?).and_return(true)
diff --git a/spec/migrations/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb b/spec/migrations/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb
index 1450811b3b9..b74e15d804f 100644
--- a/spec/migrations/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb
+++ b/spec/migrations/20220606082910_add_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb
@@ -4,7 +4,8 @@ require "spec_helper"
require_migration!
-RSpec.describe AddTmpIndexForPotentiallyMisassociatedVulnerabilityOccurrences do
+RSpec.describe AddTmpIndexForPotentiallyMisassociatedVulnerabilityOccurrences,
+feature_category: :vulnerability_management do
let(:async_index) { Gitlab::Database::AsyncIndexes::PostgresAsyncIndex }
let(:index_name) { described_class::INDEX_NAME }
diff --git a/spec/migrations/20220607082910_add_sync_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb b/spec/migrations/20220607082910_add_sync_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb
index 68fac1c2221..8d3ef9a46d7 100644
--- a/spec/migrations/20220607082910_add_sync_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb
+++ b/spec/migrations/20220607082910_add_sync_tmp_index_for_potentially_misassociated_vulnerability_occurrences_spec.rb
@@ -4,7 +4,8 @@ require "spec_helper"
require_migration!
-RSpec.describe AddSyncTmpIndexForPotentiallyMisassociatedVulnerabilityOccurrences do
+RSpec.describe AddSyncTmpIndexForPotentiallyMisassociatedVulnerabilityOccurrences,
+feature_category: :vulnerability_management do
let(:table) { "vulnerability_occurrences" }
let(:index) { described_class::INDEX_NAME }
diff --git a/spec/migrations/20220620132300_update_last_run_date_for_iterations_cadences_spec.rb b/spec/migrations/20220620132300_update_last_run_date_for_iterations_cadences_spec.rb
index d23ca8741a2..5ac4bba4cb5 100644
--- a/spec/migrations/20220620132300_update_last_run_date_for_iterations_cadences_spec.rb
+++ b/spec/migrations/20220620132300_update_last_run_date_for_iterations_cadences_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe UpdateLastRunDateForIterationsCadences, :migration do
+RSpec.describe UpdateLastRunDateForIterationsCadences, :migration, feature_category: :team_planning do
let(:current_date) { Date.parse(ApplicationRecord.connection.execute("SELECT CURRENT_DATE").first["current_date"]) }
let(:namespaces) { table(:namespaces) }
let(:iterations_cadences) { table(:iterations_cadences) }
diff --git a/spec/migrations/20220622080547_backfill_project_statistics_with_container_registry_size_spec.rb b/spec/migrations/20220622080547_backfill_project_statistics_with_container_registry_size_spec.rb
index 52b75f0b8a9..3ca8c1709f3 100644
--- a/spec/migrations/20220622080547_backfill_project_statistics_with_container_registry_size_spec.rb
+++ b/spec/migrations/20220622080547_backfill_project_statistics_with_container_registry_size_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillProjectStatisticsWithContainerRegistrySize do
- let_it_be(:batched_migration) { described_class::MIGRATION_CLASS }
+RSpec.describe BackfillProjectStatisticsWithContainerRegistrySize, feature_category: :container_registry do
+ let!(:batched_migration) { described_class::MIGRATION_CLASS }
it 'does not schedule background jobs when Gitlab.com is false' do
allow(Gitlab).to receive(:com?).and_return(false)
diff --git a/spec/migrations/20220627090231_schedule_disable_legacy_open_source_license_for_inactive_public_projects_spec.rb b/spec/migrations/20220627090231_schedule_disable_legacy_open_source_license_for_inactive_public_projects_spec.rb
index 3e7f2a3457b..edefc378575 100644
--- a/spec/migrations/20220627090231_schedule_disable_legacy_open_source_license_for_inactive_public_projects_spec.rb
+++ b/spec/migrations/20220627090231_schedule_disable_legacy_open_source_license_for_inactive_public_projects_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleDisableLegacyOpenSourceLicenseForInactivePublicProjects do
+RSpec.describe ScheduleDisableLegacyOpenSourceLicenseForInactivePublicProjects, feature_category: :projects do
context 'on gitlab.com' do
let(:migration) { described_class::MIGRATION }
diff --git a/spec/migrations/20220627152642_queue_update_delayed_project_removal_to_null_for_user_namespace_spec.rb b/spec/migrations/20220627152642_queue_update_delayed_project_removal_to_null_for_user_namespace_spec.rb
index 190f1c830ae..fe46d6a8608 100644
--- a/spec/migrations/20220627152642_queue_update_delayed_project_removal_to_null_for_user_namespace_spec.rb
+++ b/spec/migrations/20220627152642_queue_update_delayed_project_removal_to_null_for_user_namespace_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe QueueUpdateDelayedProjectRemovalToNullForUserNamespace do
+RSpec.describe QueueUpdateDelayedProjectRemovalToNullForUserNamespace, feature_category: :subgroups do
let(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/20220628012902_finalise_project_namespace_members_spec.rb b/spec/migrations/20220628012902_finalise_project_namespace_members_spec.rb
index 1f116cf6a7e..55cabc21997 100644
--- a/spec/migrations/20220628012902_finalise_project_namespace_members_spec.rb
+++ b/spec/migrations/20220628012902_finalise_project_namespace_members_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe FinaliseProjectNamespaceMembers, :migration do
+RSpec.describe FinaliseProjectNamespaceMembers, :migration, feature_category: :subgroups do
let(:batched_migrations) { table(:batched_background_migrations) }
- let_it_be(:migration) { described_class::MIGRATION }
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
shared_examples 'finalizes the migration' do
diff --git a/spec/migrations/20220629184402_unset_escalation_policies_for_alert_incidents_spec.rb b/spec/migrations/20220629184402_unset_escalation_policies_for_alert_incidents_spec.rb
index bd821714605..e01cca038ea 100644
--- a/spec/migrations/20220629184402_unset_escalation_policies_for_alert_incidents_spec.rb
+++ b/spec/migrations/20220629184402_unset_escalation_policies_for_alert_incidents_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe UnsetEscalationPoliciesForAlertIncidents do
+RSpec.describe UnsetEscalationPoliciesForAlertIncidents, feature_category: :incident_management do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:issues) { table(:issues) }
@@ -45,7 +45,7 @@ RSpec.describe UnsetEscalationPoliciesForAlertIncidents do
private
def create_issue
- issues.create!(project_id: project.id)
+ issues.create!(project_id: project.id, namespace_id: project.project_namespace_id)
end
def create_status(issue, policy = nil, escalations_started_at = nil)
diff --git a/spec/migrations/20220715163254_update_notes_in_past_spec.rb b/spec/migrations/20220715163254_update_notes_in_past_spec.rb
index 58e6cabc129..6250229a1f9 100644
--- a/spec/migrations/20220715163254_update_notes_in_past_spec.rb
+++ b/spec/migrations/20220715163254_update_notes_in_past_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe UpdateNotesInPast, :migration do
+RSpec.describe UpdateNotesInPast, :migration, feature_category: :team_planning do
let(:notes) { table(:notes) }
it 'updates created_at when it is too much in the past' do
diff --git a/spec/migrations/20220721031446_schedule_disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb b/spec/migrations/20220721031446_schedule_disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb
index b17a0215f4e..2dff9eb90cd 100644
--- a/spec/migrations/20220721031446_schedule_disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb
+++ b/spec/migrations/20220721031446_schedule_disable_legacy_open_source_license_for_one_member_no_repo_projects_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleDisableLegacyOpenSourceLicenseForOneMemberNoRepoProjects do
+RSpec.describe ScheduleDisableLegacyOpenSourceLicenseForOneMemberNoRepoProjects, feature_category: :projects do
context 'when on gitlab.com' do
let(:migration) { described_class::MIGRATION }
diff --git a/spec/migrations/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb b/spec/migrations/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
index cb0f941aea1..a994ddad850 100644
--- a/spec/migrations/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
+++ b/spec/migrations/20220722084543_schedule_disable_legacy_open_source_license_for_no_issues_no_repo_projects_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleDisableLegacyOpenSourceLicenseForNoIssuesNoRepoProjects do
+RSpec.describe ScheduleDisableLegacyOpenSourceLicenseForNoIssuesNoRepoProjects, feature_category: :projects do
context 'when on gitlab.com' do
let(:migration) { described_class::MIGRATION }
diff --git a/spec/migrations/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb b/spec/migrations/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb
index 99a30c7f2a9..ab246ea1b10 100644
--- a/spec/migrations/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb
+++ b/spec/migrations/20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe RescheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects do
+RSpec.describe RescheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects, feature_category: :projects do
context 'when on gitlab.com' do
let(:migration) { described_class::MIGRATION }
diff --git a/spec/migrations/20220725150127_update_jira_tracker_data_deployment_type_based_on_url_spec.rb b/spec/migrations/20220725150127_update_jira_tracker_data_deployment_type_based_on_url_spec.rb
index 2651e46ba53..1bd186a77e7 100644
--- a/spec/migrations/20220725150127_update_jira_tracker_data_deployment_type_based_on_url_spec.rb
+++ b/spec/migrations/20220725150127_update_jira_tracker_data_deployment_type_based_on_url_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe UpdateJiraTrackerDataDeploymentTypeBasedOnUrl, :migration do
+RSpec.describe UpdateJiraTrackerDataDeploymentTypeBasedOnUrl, :migration, feature_category: :integrations do
let(:integrations_table) { table(:integrations) }
let(:service_jira_cloud) { integrations_table.create!(id: 1, type_new: 'JiraService') }
let(:service_jira_server) { integrations_table.create!(id: 2, type_new: 'JiraService') }
diff --git a/spec/migrations/20220801155858_schedule_disable_legacy_open_source_licence_for_recent_public_projects_spec.rb b/spec/migrations/20220801155858_schedule_disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
index fdd97f2d008..f8f1565fe4c 100644
--- a/spec/migrations/20220801155858_schedule_disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
+++ b/spec/migrations/20220801155858_schedule_disable_legacy_open_source_licence_for_recent_public_projects_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleDisableLegacyOpenSourceLicenceForRecentPublicProjects, schema: 20220801155858 do
+RSpec.describe ScheduleDisableLegacyOpenSourceLicenceForRecentPublicProjects, schema: 20220801155858,
+ feature_category: :projects do
context 'when on gitlab.com' do
let(:background_migration) { described_class::MIGRATION }
let(:migration) { described_class.new }
diff --git a/spec/migrations/20220802114351_reschedule_backfill_container_registry_size_into_project_statistics_spec.rb b/spec/migrations/20220802114351_reschedule_backfill_container_registry_size_into_project_statistics_spec.rb
index cc1c1dac4c3..35d0cdfa25e 100644
--- a/spec/migrations/20220802114351_reschedule_backfill_container_registry_size_into_project_statistics_spec.rb
+++ b/spec/migrations/20220802114351_reschedule_backfill_container_registry_size_into_project_statistics_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe RescheduleBackfillContainerRegistrySizeIntoProjectStatistics do
- let_it_be(:batched_migration) { described_class::MIGRATION_CLASS }
+RSpec.describe RescheduleBackfillContainerRegistrySizeIntoProjectStatistics, feature_category: :container_registry do
+ let!(:batched_migration) { described_class::MIGRATION_CLASS }
it 'does not schedule background jobs when Gitlab.com is false' do
allow(Gitlab).to receive(:com?).and_return(false)
diff --git a/spec/migrations/20220802204737_remove_deactivated_user_highest_role_stats_spec.rb b/spec/migrations/20220802204737_remove_deactivated_user_highest_role_stats_spec.rb
index 3ea286ca138..dd77ce503b8 100644
--- a/spec/migrations/20220802204737_remove_deactivated_user_highest_role_stats_spec.rb
+++ b/spec/migrations/20220802204737_remove_deactivated_user_highest_role_stats_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe RemoveDeactivatedUserHighestRoleStats do
+RSpec.describe RemoveDeactivatedUserHighestRoleStats, feature_category: :utilization do
let!(:users) { table(:users) }
let!(:user_highest_roles) { table(:user_highest_roles) }
diff --git a/spec/migrations/20220816163444_update_start_date_for_iterations_cadences_spec.rb b/spec/migrations/20220816163444_update_start_date_for_iterations_cadences_spec.rb
index 5a5e2362a53..25b2b5c2e18 100644
--- a/spec/migrations/20220816163444_update_start_date_for_iterations_cadences_spec.rb
+++ b/spec/migrations/20220816163444_update_start_date_for_iterations_cadences_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe UpdateStartDateForIterationsCadences, :freeze_time do
+RSpec.describe UpdateStartDateForIterationsCadences, :freeze_time, feature_category: :team_planning do
let(:migration) { described_class.new }
let(:namespaces) { table(:namespaces) }
let(:sprints) { table(:sprints) }
diff --git a/spec/migrations/20220819153725_add_vulnerability_advisory_foreign_key_to_sbom_vulnerable_component_versions_spec.rb b/spec/migrations/20220819153725_add_vulnerability_advisory_foreign_key_to_sbom_vulnerable_component_versions_spec.rb
index c53dd9de649..5a61f49485c 100644
--- a/spec/migrations/20220819153725_add_vulnerability_advisory_foreign_key_to_sbom_vulnerable_component_versions_spec.rb
+++ b/spec/migrations/20220819153725_add_vulnerability_advisory_foreign_key_to_sbom_vulnerable_component_versions_spec.rb
@@ -4,7 +4,8 @@ require "spec_helper"
require_migration!
-RSpec.describe AddVulnerabilityAdvisoryForeignKeyToSbomVulnerableComponentVersions do
+RSpec.describe AddVulnerabilityAdvisoryForeignKeyToSbomVulnerableComponentVersions,
+feature_category: :dependency_management do
let(:table) { described_class::SOURCE_TABLE }
let(:column) { described_class::COLUMN }
let(:foreign_key) { -> { described_class.new.foreign_keys_for(table, column).first } }
diff --git a/spec/migrations/20220819162852_add_sbom_component_version_foreign_key_to_sbom_vulnerable_component_versions_spec.rb b/spec/migrations/20220819162852_add_sbom_component_version_foreign_key_to_sbom_vulnerable_component_versions_spec.rb
index b9cb6891681..999c833f9e3 100644
--- a/spec/migrations/20220819162852_add_sbom_component_version_foreign_key_to_sbom_vulnerable_component_versions_spec.rb
+++ b/spec/migrations/20220819162852_add_sbom_component_version_foreign_key_to_sbom_vulnerable_component_versions_spec.rb
@@ -4,7 +4,8 @@ require "spec_helper"
require_migration!
-RSpec.describe AddSbomComponentVersionForeignKeyToSbomVulnerableComponentVersions do
+RSpec.describe AddSbomComponentVersionForeignKeyToSbomVulnerableComponentVersions,
+feature_category: :dependency_management do
let(:table) { described_class::SOURCE_TABLE }
let(:column) { described_class::COLUMN }
let(:foreign_key) { -> { described_class.new.foreign_keys_for(table, column).first } }
diff --git a/spec/migrations/20220906074449_schedule_disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb b/spec/migrations/20220906074449_schedule_disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
index e4ac094ab48..852748bcdc1 100644
--- a/spec/migrations/20220906074449_schedule_disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
+++ b/spec/migrations/20220906074449_schedule_disable_legacy_open_source_license_for_projects_less_than_one_mb_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleDisableLegacyOpenSourceLicenseForProjectsLessThanOneMb do
- let_it_be(:migration) { described_class.new }
- let_it_be(:post_migration) { described_class::MIGRATION }
+RSpec.describe ScheduleDisableLegacyOpenSourceLicenseForProjectsLessThanOneMb, feature_category: :projects do
+ let!(:migration) { described_class.new }
+ let!(:post_migration) { described_class::MIGRATION }
context 'when on gitlab.com' do
before do
diff --git a/spec/migrations/20220913030624_cleanup_attention_request_related_system_notes_spec.rb b/spec/migrations/20220913030624_cleanup_attention_request_related_system_notes_spec.rb
index 7338a6ab9ae..03e53a406ed 100644
--- a/spec/migrations/20220913030624_cleanup_attention_request_related_system_notes_spec.rb
+++ b/spec/migrations/20220913030624_cleanup_attention_request_related_system_notes_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupAttentionRequestRelatedSystemNotes, :migration do
+RSpec.describe CleanupAttentionRequestRelatedSystemNotes, :migration, feature_category: :team_planning do
let(:notes) { table(:notes) }
let(:system_note_metadata) { table(:system_note_metadata) }
diff --git a/spec/migrations/20220920124709_backfill_internal_on_notes_spec.rb b/spec/migrations/20220920124709_backfill_internal_on_notes_spec.rb
index f4ac6e6fc8e..6e3a058f245 100644
--- a/spec/migrations/20220920124709_backfill_internal_on_notes_spec.rb
+++ b/spec/migrations/20220920124709_backfill_internal_on_notes_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillInternalOnNotes, :migration do
+RSpec.describe BackfillInternalOnNotes, :migration, feature_category: :team_planning do
let(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/20220920180451_schedule_vulnerabilities_feedback_migration_spec.rb b/spec/migrations/20220920180451_schedule_vulnerabilities_feedback_migration_spec.rb
new file mode 100644
index 00000000000..4f2b5f6b50f
--- /dev/null
+++ b/spec/migrations/20220920180451_schedule_vulnerabilities_feedback_migration_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleVulnerabilitiesFeedbackMigration, feature_category: :vulnerability_management do
+ let(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of Vulnerabilities::Feedback' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :vulnerability_feedback,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ max_batch_size: described_class::MAX_BATCH_SIZE
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/20220921093355_schedule_backfill_namespace_details_spec.rb b/spec/migrations/20220921093355_schedule_backfill_namespace_details_spec.rb
index 61e4af3d10c..5ac49762dbf 100644
--- a/spec/migrations/20220921093355_schedule_backfill_namespace_details_spec.rb
+++ b/spec/migrations/20220921093355_schedule_backfill_namespace_details_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleBackfillNamespaceDetails, schema: 20220921093355 do
+RSpec.describe ScheduleBackfillNamespaceDetails, schema: 20220921093355, feature_category: :subgroups do
context 'when on gitlab.com' do
let(:background_migration) { described_class::MIGRATION }
let(:migration) { described_class.new }
diff --git a/spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb b/spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb
index 174cfda1a46..19cf3b2fb69 100644
--- a/spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb
+++ b/spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
require_migration!
-RSpec.describe RemoveOrphanGroupTokenUsers, :migration, :sidekiq_inline do
+RSpec.describe RemoveOrphanGroupTokenUsers, :migration, :sidekiq_inline,
+feature_category: :authentication_and_authorization do
subject(:migration) { described_class.new }
let(:users) { table(:users) }
diff --git a/spec/migrations/20220922143143_schedule_reset_duplicate_ci_runners_token_values_spec.rb b/spec/migrations/20220922143143_schedule_reset_duplicate_ci_runners_token_values_spec.rb
index 409f7d544ee..07627725ed0 100644
--- a/spec/migrations/20220922143143_schedule_reset_duplicate_ci_runners_token_values_spec.rb
+++ b/spec/migrations/20220922143143_schedule_reset_duplicate_ci_runners_token_values_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleResetDuplicateCiRunnersTokenValues, migration: :gitlab_ci do
+RSpec.describe ScheduleResetDuplicateCiRunnersTokenValues, feature_category: :runner_fleet, migration: :gitlab_ci do
let(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/20220922143634_schedule_reset_duplicate_ci_runners_token_encrypted_values_spec.rb b/spec/migrations/20220922143634_schedule_reset_duplicate_ci_runners_token_encrypted_values_spec.rb
index 4f3103927d5..42f200e0d6f 100644
--- a/spec/migrations/20220922143634_schedule_reset_duplicate_ci_runners_token_encrypted_values_spec.rb
+++ b/spec/migrations/20220922143634_schedule_reset_duplicate_ci_runners_token_encrypted_values_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleResetDuplicateCiRunnersTokenEncryptedValues, migration: :gitlab_ci do
+RSpec.describe ScheduleResetDuplicateCiRunnersTokenEncryptedValues,
+ feature_category: :runner_fleet,
+ migration: :gitlab_ci do
let(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/20220928225711_schedule_update_ci_pipeline_artifacts_locked_status_spec.rb b/spec/migrations/20220928225711_schedule_update_ci_pipeline_artifacts_locked_status_spec.rb
index 7e3f8caa966..5c1b5c8f2a7 100644
--- a/spec/migrations/20220928225711_schedule_update_ci_pipeline_artifacts_locked_status_spec.rb
+++ b/spec/migrations/20220928225711_schedule_update_ci_pipeline_artifacts_locked_status_spec.rb
@@ -3,8 +3,9 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleUpdateCiPipelineArtifactsLockedStatus, migration: :gitlab_ci do
- let_it_be(:migration) { described_class::MIGRATION }
+RSpec.describe ScheduleUpdateCiPipelineArtifactsLockedStatus, migration: :gitlab_ci,
+ feature_category: :build_artifacts do
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of ci_pipeline_artifacts' do
diff --git a/spec/migrations/20220929213730_schedule_delete_orphaned_operational_vulnerabilities_spec.rb b/spec/migrations/20220929213730_schedule_delete_orphaned_operational_vulnerabilities_spec.rb
index 9220b5e8a95..2e391868060 100644
--- a/spec/migrations/20220929213730_schedule_delete_orphaned_operational_vulnerabilities_spec.rb
+++ b/spec/migrations/20220929213730_schedule_delete_orphaned_operational_vulnerabilities_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleDeleteOrphanedOperationalVulnerabilities do
- let_it_be(:migration) { described_class.new }
- let_it_be(:post_migration) { described_class::MIGRATION }
+RSpec.describe ScheduleDeleteOrphanedOperationalVulnerabilities, feature_category: :vulnerability_management do
+ let!(:migration) { described_class.new }
+ let!(:post_migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of vulnerabilities' do
diff --git a/spec/migrations/20221002234454_finalize_group_member_namespace_id_migration_spec.rb b/spec/migrations/20221002234454_finalize_group_member_namespace_id_migration_spec.rb
index 9c27005065d..4ff16111417 100644
--- a/spec/migrations/20221002234454_finalize_group_member_namespace_id_migration_spec.rb
+++ b/spec/migrations/20221002234454_finalize_group_member_namespace_id_migration_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe FinalizeGroupMemberNamespaceIdMigration, :migration do
+RSpec.describe FinalizeGroupMemberNamespaceIdMigration, :migration, feature_category: :subgroups do
let(:batched_migrations) { table(:batched_background_migrations) }
- let_it_be(:migration) { described_class::MIGRATION }
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
shared_examples 'finalizes the migration' do
diff --git a/spec/migrations/20221004094814_schedule_destroy_invalid_members_spec.rb b/spec/migrations/20221004094814_schedule_destroy_invalid_members_spec.rb
index 73fdfa78eb4..8bffa4b9b99 100644
--- a/spec/migrations/20221004094814_schedule_destroy_invalid_members_spec.rb
+++ b/spec/migrations/20221004094814_schedule_destroy_invalid_members_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleDestroyInvalidMembers, :migration do
- let_it_be(:migration) { described_class::MIGRATION }
+RSpec.describe ScheduleDestroyInvalidMembers, :migration, feature_category: :subgroups do
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of members' do
diff --git a/spec/migrations/20221008032350_add_password_expiration_migration_spec.rb b/spec/migrations/20221008032350_add_password_expiration_migration_spec.rb
index 05e557f1f52..1663966816c 100644
--- a/spec/migrations/20221008032350_add_password_expiration_migration_spec.rb
+++ b/spec/migrations/20221008032350_add_password_expiration_migration_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddPasswordExpirationMigration do
+RSpec.describe AddPasswordExpirationMigration, feature_category: :users do
let(:application_setting) { table(:application_settings).create! }
describe "#up" do
diff --git a/spec/migrations/20221012033107_add_password_last_changed_at_to_user_details_spec.rb b/spec/migrations/20221012033107_add_password_last_changed_at_to_user_details_spec.rb
index 46a7b097d02..e2c508938fd 100644
--- a/spec/migrations/20221012033107_add_password_last_changed_at_to_user_details_spec.rb
+++ b/spec/migrations/20221012033107_add_password_last_changed_at_to_user_details_spec.rb
@@ -4,10 +4,10 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddPasswordLastChangedAtToUserDetails do
- let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
- let_it_be(:users) { table(:users) }
- let_it_be(:user) { create_user! }
+RSpec.describe AddPasswordLastChangedAtToUserDetails, feature_category: :users do
+ let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let!(:users) { table(:users) }
+ let!(:user) { create_user! }
let(:user_detail) { table(:user_details).create!(user_id: user.id, provisioned_by_group_id: namespace.id) }
describe "#up" do
diff --git a/spec/migrations/20221013154159_update_invalid_dormant_user_setting_spec.rb b/spec/migrations/20221013154159_update_invalid_dormant_user_setting_spec.rb
index eac71e428be..0686d9ca786 100644
--- a/spec/migrations/20221013154159_update_invalid_dormant_user_setting_spec.rb
+++ b/spec/migrations/20221013154159_update_invalid_dormant_user_setting_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe UpdateInvalidDormantUserSetting, :migration do
+RSpec.describe UpdateInvalidDormantUserSetting, :migration, feature_category: :users do
let(:settings) { table(:application_settings) }
context 'with no rows in the application_settings table' do
diff --git a/spec/migrations/20221018050323_add_objective_and_keyresult_to_work_item_types_spec.rb b/spec/migrations/20221018050323_add_objective_and_keyresult_to_work_item_types_spec.rb
index 4de897802b9..3ab33367303 100644
--- a/spec/migrations/20221018050323_add_objective_and_keyresult_to_work_item_types_spec.rb
+++ b/spec/migrations/20221018050323_add_objective_and_keyresult_to_work_item_types_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe AddObjectiveAndKeyresultToWorkItemTypes, :migration do
+RSpec.describe AddObjectiveAndKeyresultToWorkItemTypes, :migration, feature_category: :team_planning do
include MigrationHelpers::WorkItemTypesHelper
- let_it_be(:work_item_types) { table(:work_item_types) }
+ let!(:work_item_types) { table(:work_item_types) }
let(:base_types) do
{
diff --git a/spec/migrations/20221018062308_schedule_backfill_project_namespace_details_spec.rb b/spec/migrations/20221018062308_schedule_backfill_project_namespace_details_spec.rb
index 4dd6d5757ce..4175d9b1ad8 100644
--- a/spec/migrations/20221018062308_schedule_backfill_project_namespace_details_spec.rb
+++ b/spec/migrations/20221018062308_schedule_backfill_project_namespace_details_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleBackfillProjectNamespaceDetails, schema: 20221018062308 do
+RSpec.describe ScheduleBackfillProjectNamespaceDetails, schema: 20221018062308, feature_category: :projects do
context 'when on gitlab.com' do
- let_it_be(:background_migration) { described_class::MIGRATION }
- let_it_be(:migration) { described_class.new }
+ let!(:background_migration) { described_class::MIGRATION }
+ let!(:migration) { described_class.new }
before do
migration.up
diff --git a/spec/migrations/20221018095434_schedule_disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb b/spec/migrations/20221018095434_schedule_disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb
new file mode 100644
index 00000000000..34bba8ed9c8
--- /dev/null
+++ b/spec/migrations/20221018095434_schedule_disable_legacy_open_source_license_for_projects_less_than_five_mb_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleDisableLegacyOpenSourceLicenseForProjectsLessThanFiveMb, feature_category: :projects do
+ let!(:migration) { described_class.new }
+ let!(:post_migration) { described_class::MIGRATION }
+
+ context 'when on gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of project_settings' do
+ migration.up
+
+ expect(post_migration).to(
+ have_scheduled_batched_migration(
+ table_name: :project_settings,
+ column_name: :project_id,
+ interval: described_class::INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ max_batch_size: described_class::MAX_BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migration.down
+
+ expect(post_migration).not_to have_scheduled_batched_migration
+ end
+ end
+ end
+
+ context 'when on self-managed instance' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ describe '#up' do
+ it 'does not schedule background job' do
+ expect(migration).not_to receive(:queue_batched_background_migration)
+
+ migration.up
+ end
+ end
+
+ describe '#down' do
+ it 'does not delete background job' do
+ expect(migration).not_to receive(:delete_batched_background_migration)
+
+ migration.down
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20221018193635_ensure_task_note_renaming_background_migration_finished_spec.rb b/spec/migrations/20221018193635_ensure_task_note_renaming_background_migration_finished_spec.rb
index ea95c34674e..8b599881359 100644
--- a/spec/migrations/20221018193635_ensure_task_note_renaming_background_migration_finished_spec.rb
+++ b/spec/migrations/20221018193635_ensure_task_note_renaming_background_migration_finished_spec.rb
@@ -3,12 +3,12 @@
require 'spec_helper'
require_migration!
-RSpec.describe EnsureTaskNoteRenamingBackgroundMigrationFinished, :migration do
+RSpec.describe EnsureTaskNoteRenamingBackgroundMigrationFinished, :migration, feature_category: :team_planning do
let(:batched_migrations) { table(:batched_background_migrations) }
let(:batch_failed_status) { 2 }
let(:batch_finalized_status) { 3 }
- let_it_be(:migration) { described_class::MIGRATION }
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
shared_examples 'finalizes the migration' do
diff --git a/spec/migrations/20221021145820_create_routing_table_for_builds_metadata_v2_spec.rb b/spec/migrations/20221021145820_create_routing_table_for_builds_metadata_v2_spec.rb
index 48a00df430d..235351956c4 100644
--- a/spec/migrations/20221021145820_create_routing_table_for_builds_metadata_v2_spec.rb
+++ b/spec/migrations/20221021145820_create_routing_table_for_builds_metadata_v2_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe CreateRoutingTableForBuildsMetadataV2, :migration do
- let_it_be(:migration) { described_class.new }
+RSpec.describe CreateRoutingTableForBuildsMetadataV2, :migration, feature_category: :continuous_integration do
+ let!(:migration) { described_class.new }
describe '#up' do
context 'when the table is already partitioned' do
diff --git a/spec/migrations/20221025043930_change_default_value_on_password_last_changed_at_to_user_details_spec.rb b/spec/migrations/20221025043930_change_default_value_on_password_last_changed_at_to_user_details_spec.rb
index 4d6f06eb146..d6acf31fdc6 100644
--- a/spec/migrations/20221025043930_change_default_value_on_password_last_changed_at_to_user_details_spec.rb
+++ b/spec/migrations/20221025043930_change_default_value_on_password_last_changed_at_to_user_details_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ChangeDefaultValueOnPasswordLastChangedAtToUserDetails, :migration do
+RSpec.describe ChangeDefaultValueOnPasswordLastChangedAtToUserDetails, :migration, feature_category: :users do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let(:users) { table(:users) }
let(:user_details) { table(:user_details) }
diff --git a/spec/migrations/20221028022627_add_index_on_password_last_changed_at_to_user_details_spec.rb b/spec/migrations/20221028022627_add_index_on_password_last_changed_at_to_user_details_spec.rb
index 5f8467f9307..6b6fb553b1f 100644
--- a/spec/migrations/20221028022627_add_index_on_password_last_changed_at_to_user_details_spec.rb
+++ b/spec/migrations/20221028022627_add_index_on_password_last_changed_at_to_user_details_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe AddIndexOnPasswordLastChangedAtToUserDetails, :migration do
+RSpec.describe AddIndexOnPasswordLastChangedAtToUserDetails, :migration, feature_category: :users do
let(:index_name) { 'index_user_details_on_password_last_changed_at' }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20221101032521_add_default_preferred_language_to_application_settings_spec.rb b/spec/migrations/20221101032521_add_default_preferred_language_to_application_settings_spec.rb
index 3ae4287f3c4..deca498146b 100644
--- a/spec/migrations/20221101032521_add_default_preferred_language_to_application_settings_spec.rb
+++ b/spec/migrations/20221101032521_add_default_preferred_language_to_application_settings_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddDefaultPreferredLanguageToApplicationSettings do
+RSpec.describe AddDefaultPreferredLanguageToApplicationSettings, feature_category: :internationalization do
let(:application_setting) { table(:application_settings).create! }
describe "#up" do
diff --git a/spec/migrations/20221101032600_add_text_limit_to_default_preferred_language_on_application_settings_spec.rb b/spec/migrations/20221101032600_add_text_limit_to_default_preferred_language_on_application_settings_spec.rb
index e0370e48db6..3e36e99a0ca 100644
--- a/spec/migrations/20221101032600_add_text_limit_to_default_preferred_language_on_application_settings_spec.rb
+++ b/spec/migrations/20221101032600_add_text_limit_to_default_preferred_language_on_application_settings_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddTextLimitToDefaultPreferredLanguageOnApplicationSettings do
+RSpec.describe AddTextLimitToDefaultPreferredLanguageOnApplicationSettings, feature_category: :internationalization do
let(:application_setting) { table(:application_settings).create! }
let(:too_long_text) { SecureRandom.alphanumeric(described_class::MAXIMUM_LIMIT + 1) }
diff --git a/spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb b/spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb
index c55e4bcfba7..dc6f365fe2b 100644
--- a/spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb
+++ b/spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CreateNextCiPartitionsRecord, migration: :gitlab_ci do
+RSpec.describe CreateNextCiPartitionsRecord, migration: :gitlab_ci, feature_category: :continuous_integration do
let(:migration) { described_class.new }
let(:partitions) { table(:ci_partitions) }
diff --git a/spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb b/spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb
index 99754d609ed..b4bd5136383 100644
--- a/spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb
+++ b/spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CreateSecondPartitionForBuildsMetadata, :migration do
+RSpec.describe CreateSecondPartitionForBuildsMetadata, :migration, feature_category: :continuous_integration do
let(:migration) { described_class.new }
let(:partitions) { table(:ci_partitions) }
diff --git a/spec/migrations/20221104115712_backfill_project_statistics_storage_size_without_uploads_size_spec.rb b/spec/migrations/20221104115712_backfill_project_statistics_storage_size_without_uploads_size_spec.rb
new file mode 100644
index 00000000000..d86720365c4
--- /dev/null
+++ b/spec/migrations/20221104115712_backfill_project_statistics_storage_size_without_uploads_size_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillProjectStatisticsStorageSizeWithoutUploadsSize,
+ feature_category: :subscription_cost_management do
+ let!(:batched_migration) { described_class::MIGRATION_CLASS }
+
+ it 'does not schedule background jobs when Gitlab.org_or_com? is false' do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+ allow(Gitlab).to receive(:org_or_com?).and_return(false)
+
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+ end
+ end
+
+ it 'schedules background jobs for each batch of project_statistics' do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+ allow(Gitlab).to receive(:org_or_com?).and_return(true)
+
+ 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: :project_statistics,
+ column_name: :project_id,
+ interval: described_class::DELAY_INTERVAL
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20221110152133_delete_orphans_approval_rules_spec.rb b/spec/migrations/20221110152133_delete_orphans_approval_rules_spec.rb
new file mode 100644
index 00000000000..3efee67f7af
--- /dev/null
+++ b/spec/migrations/20221110152133_delete_orphans_approval_rules_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe DeleteOrphansApprovalRules, feature_category: :source_code_management do
+ describe '#up' do
+ it 'schedules background migration for both levels of approval rules' do
+ migrate!
+
+ expect(described_class::MERGE_REQUEST_MIGRATION).to have_scheduled_batched_migration(
+ table_name: :approval_merge_request_rules,
+ column_name: :id,
+ interval: described_class::INTERVAL)
+
+ expect(described_class::PROJECT_MIGRATION).to have_scheduled_batched_migration(
+ table_name: :approval_project_rules,
+ column_name: :id,
+ interval: described_class::INTERVAL)
+ end
+ end
+end
diff --git a/spec/migrations/20221115173607_ensure_work_item_type_backfill_migration_finished_spec.rb b/spec/migrations/20221115173607_ensure_work_item_type_backfill_migration_finished_spec.rb
new file mode 100644
index 00000000000..e9250625832
--- /dev/null
+++ b/spec/migrations/20221115173607_ensure_work_item_type_backfill_migration_finished_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe EnsureWorkItemTypeBackfillMigrationFinished, :migration, feature_category: :team_planning do
+ let(:batched_migrations) { table(:batched_background_migrations) }
+ let(:work_item_types) { table(:work_item_types) }
+ let(:batch_failed_status) { 2 }
+ let(:batch_finalized_status) { 3 }
+
+ let!(:migration_class) { described_class::MIGRATION }
+
+ describe '#up', :redis do
+ context 'when migration is missing' do
+ it 'warns migration not found' do
+ expect(Gitlab::AppLogger)
+ .to receive(:warn).with(/Could not find batched background migration for the given configuration:/)
+ .exactly(5).times
+
+ migrate!
+ end
+ end
+
+ context 'with migration present' do
+ let(:relevant_types) do
+ {
+ issue: 0,
+ incident: 1,
+ test_case: 2,
+ requirement: 3,
+ task: 4
+ }
+ end
+
+ let!(:backfill_migrations) do
+ relevant_types.map do |_base_type, enum_value|
+ type_id = work_item_types.find_by!(namespace_id: nil, base_type: enum_value).id
+
+ create_migration_with(status, enum_value, type_id)
+ end
+ end
+
+ context 'when migrations have finished' do
+ let(:status) { 3 } # finished enum value
+
+ it 'does not raise an error' do
+ expect { migrate! }.not_to raise_error
+ end
+ end
+
+ context 'with different migration statuses' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :description) do
+ 0 | 'paused'
+ 1 | 'active'
+ 4 | 'failed'
+ 5 | 'finalizing'
+ end
+
+ with_them do
+ it 'finalizes the migration' do
+ expect do
+ migrate!
+
+ backfill_migrations.each(&:reload)
+ end.to change { backfill_migrations.map(&:status).uniq }.from([status]).to([3])
+ end
+ end
+ end
+ end
+ end
+
+ def create_migration_with(status, base_type, type_id)
+ migration = batched_migrations.create!(
+ job_class_name: migration_class,
+ table_name: :issues,
+ column_name: :id,
+ job_arguments: [base_type, type_id],
+ interval: 2.minutes,
+ min_value: 1,
+ max_value: 2,
+ batch_size: 1000,
+ sub_batch_size: 200,
+ gitlab_schema: :gitlab_main,
+ status: status
+ )
+
+ table(:batched_background_migration_jobs).create!(
+ batched_background_migration_id: migration.id,
+ status: batch_failed_status,
+ min_value: 1,
+ max_value: 10,
+ attempts: 2,
+ batch_size: 100,
+ sub_batch_size: 10
+ )
+
+ migration
+ end
+end
diff --git a/spec/migrations/20221122132812_schedule_prune_stale_project_export_jobs_spec.rb b/spec/migrations/20221122132812_schedule_prune_stale_project_export_jobs_spec.rb
new file mode 100644
index 00000000000..5a5bc42a37b
--- /dev/null
+++ b/spec/migrations/20221122132812_schedule_prune_stale_project_export_jobs_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SchedulePruneStaleProjectExportJobs, category: :importers 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: :project_export_jobs,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20221123133054_queue_reset_status_on_container_repositories_spec.rb b/spec/migrations/20221123133054_queue_reset_status_on_container_repositories_spec.rb
new file mode 100644
index 00000000000..2951b738243
--- /dev/null
+++ b/spec/migrations/20221123133054_queue_reset_status_on_container_repositories_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueResetStatusOnContainerRepositories, feature_category: :container_registry do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ before do
+ stub_container_registry_config(
+ enabled: true,
+ api_url: 'http://example.com',
+ key: 'spec/fixtures/x509_certificate_pk.key'
+ )
+ end
+
+ 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: :container_repositories,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ sub_batch_size: described_class::BATCH_SIZE
+ )
+ }
+ end
+ end
+
+ context 'with the container registry disabled' do
+ before do
+ allow(::Gitlab.config.registry).to receive(:enabled).and_return(false)
+ end
+
+ it 'does not schedule 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).not_to have_scheduled_batched_migration
+ }
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20221205151917_schedule_backfill_environment_tier_spec.rb b/spec/migrations/20221205151917_schedule_backfill_environment_tier_spec.rb
new file mode 100644
index 00000000000..b76f889d743
--- /dev/null
+++ b/spec/migrations/20221205151917_schedule_backfill_environment_tier_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleBackfillEnvironmentTier, category: :continuous_delivery 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: :environments,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20221209110934_update_import_sources_on_application_settings_spec.rb b/spec/migrations/20221209110934_update_import_sources_on_application_settings_spec.rb
new file mode 100644
index 00000000000..899074399a1
--- /dev/null
+++ b/spec/migrations/20221209110934_update_import_sources_on_application_settings_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdateImportSourcesOnApplicationSettings, feature_category: :migration do
+ let(:settings) { table(:application_settings) }
+ let(:import_sources_with_google) { %w[google_code github git bitbucket bitbucket_server] }
+ let(:import_sources_without_google) { %w[github git bitbucket bitbucket_server] }
+
+ describe "#up" do
+ it 'removes google_code and preserves existing valid import sources' do
+ record = settings.create!(import_sources: import_sources_with_google.to_yaml)
+
+ migrate!
+
+ expect(record.reload.import_sources).to start_with('---')
+ expect(ApplicationSetting.last.import_sources).to eq(import_sources_without_google)
+ end
+ end
+end
diff --git a/spec/migrations/20221209110935_fix_update_import_sources_on_application_settings_spec.rb b/spec/migrations/20221209110935_fix_update_import_sources_on_application_settings_spec.rb
new file mode 100644
index 00000000000..e5b20b2d48a
--- /dev/null
+++ b/spec/migrations/20221209110935_fix_update_import_sources_on_application_settings_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FixUpdateImportSourcesOnApplicationSettings, feature_category: :migration do
+ let(:settings) { table(:application_settings) }
+ let(:import_sources) { %w[github git bitbucket bitbucket_server] }
+
+ describe "#up" do
+ shared_examples 'fixes import_sources on application_settings' do
+ it 'ensures YAML is stored' do
+ record = settings.create!(import_sources: data)
+
+ migrate!
+
+ expect(record.reload.import_sources).to start_with('---')
+ expect(ApplicationSetting.last.import_sources).to eq(import_sources)
+ end
+ end
+
+ context 'when import_sources is a String' do
+ let(:data) { import_sources.to_s }
+
+ it_behaves_like 'fixes import_sources on application_settings'
+ end
+
+ context 'when import_sources is already YAML' do
+ let(:data) { import_sources.to_yaml }
+
+ it_behaves_like 'fixes import_sources on application_settings'
+ end
+ end
+end
diff --git a/spec/migrations/20221210154044_update_active_billable_users_index_spec.rb b/spec/migrations/20221210154044_update_active_billable_users_index_spec.rb
new file mode 100644
index 00000000000..3341df2ce51
--- /dev/null
+++ b/spec/migrations/20221210154044_update_active_billable_users_index_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdateActiveBillableUsersIndex, feature_category: :database do
+ let(:db) { described_class.new }
+ let(:table_name) { described_class::TABLE_NAME }
+ let(:old_index_name) { described_class::OLD_INDEX_NAME }
+ let(:new_index_name) { described_class::NEW_INDEX_NAME }
+ let(:old_filter_condition) { "(user_type <> ALL ('{2,6,1,3,7,8}'::smallint[])))" }
+ let(:new_filter_condition) { "(user_type <> ALL ('{1,2,3,4,5,6,7,8,9,11}'::smallint[])))" }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(subject.index_exists_by_name?(table_name, new_index_name)).to be_falsy
+ expect(subject.index_exists_by_name?(table_name, old_index_name)).to be_truthy
+ expect(db.connection.indexes(table_name).find do |i|
+ i.name == old_index_name
+ end.where).to include(old_filter_condition)
+ }
+
+ migration.after -> {
+ expect(subject.index_exists_by_name?(table_name, old_index_name)).to be_falsy
+ expect(subject.index_exists_by_name?(table_name, new_index_name)).to be_truthy
+ expect(db.connection.indexes(table_name).find do |i|
+ i.name == new_index_name
+ end.where).to include(new_filter_condition)
+ }
+ end
+ end
+end
diff --git a/spec/migrations/active_record/schema_spec.rb b/spec/migrations/active_record/schema_spec.rb
index 042b5710dce..f3adffb9a37 100644
--- a/spec/migrations/active_record/schema_spec.rb
+++ b/spec/migrations/active_record/schema_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
# Check consistency of db/structure.sql version, migrations' timestamps, and the latest migration timestamp
# stored in the database's schema_migrations table.
-RSpec.describe ActiveRecord::Schema, schema: :latest do
+RSpec.describe ActiveRecord::Schema, schema: :latest, feature_category: :database do
let(:all_migrations) do
migrations_directories = Rails.application.paths["db/migrate"].paths.map(&:to_s)
migrations_paths = migrations_directories.map { |path| File.join(path, '*') }
diff --git a/spec/migrations/add_default_project_approval_rules_vuln_allowed_spec.rb b/spec/migrations/add_default_project_approval_rules_vuln_allowed_spec.rb
index 057e95eb158..a6c892db131 100644
--- a/spec/migrations/add_default_project_approval_rules_vuln_allowed_spec.rb
+++ b/spec/migrations/add_default_project_approval_rules_vuln_allowed_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe AddDefaultProjectApprovalRulesVulnAllowed do
+RSpec.describe AddDefaultProjectApprovalRulesVulnAllowed, feature_category: :source_code_management do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') }
diff --git a/spec/migrations/add_epics_relative_position_spec.rb b/spec/migrations/add_epics_relative_position_spec.rb
index f3b7dd1727b..bdfaacc2bf8 100644
--- a/spec/migrations/add_epics_relative_position_spec.rb
+++ b/spec/migrations/add_epics_relative_position_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddEpicsRelativePosition, :migration do
+RSpec.describe AddEpicsRelativePosition, :migration, feature_category: :portfolio_management do
let(:groups) { table(:namespaces) }
let(:epics) { table(:epics) }
let(:users) { table(:users) }
diff --git a/spec/migrations/add_new_trail_plans_spec.rb b/spec/migrations/add_new_trail_plans_spec.rb
index c1b488e8c3c..6f8de8435c6 100644
--- a/spec/migrations/add_new_trail_plans_spec.rb
+++ b/spec/migrations/add_new_trail_plans_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddNewTrailPlans, :migration do
+RSpec.describe AddNewTrailPlans, :migration, feature_category: :purchase do
describe '#up' do
before do
allow(Gitlab).to receive(:com?).and_return true
diff --git a/spec/migrations/add_okr_hierarchy_restrictions_spec.rb b/spec/migrations/add_okr_hierarchy_restrictions_spec.rb
new file mode 100644
index 00000000000..ace581c7e3c
--- /dev/null
+++ b/spec/migrations/add_okr_hierarchy_restrictions_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddOkrHierarchyRestrictions, :migration, feature_category: :portfolio_management do
+ include MigrationHelpers::WorkItemTypesHelper
+
+ let!(:restrictions) { table(:work_item_hierarchy_restrictions) }
+ let!(:work_item_types) { table(:work_item_types) }
+
+ it 'creates default restrictions' do
+ restrictions.delete_all
+
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(restrictions.count).to eq(0)
+ }
+
+ migration.after -> {
+ expect(restrictions.count).to eq(4)
+ }
+ end
+ end
+
+ context 'when work items are missing' do
+ before do
+ work_item_types.delete_all
+ end
+
+ it 'does nothing' do
+ expect { migrate! }.not_to change { restrictions.count }
+ end
+ end
+end
diff --git a/spec/migrations/add_open_source_plan_spec.rb b/spec/migrations/add_open_source_plan_spec.rb
index 6e1cd544141..f5d68f455e6 100644
--- a/spec/migrations/add_open_source_plan_spec.rb
+++ b/spec/migrations/add_open_source_plan_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddOpenSourcePlan, :migration do
+RSpec.describe AddOpenSourcePlan, :migration, feature_category: :purchase do
describe '#up' do
before do
allow(Gitlab).to receive(:com?).and_return true
diff --git a/spec/migrations/add_premium_and_ultimate_plan_limits_spec.rb b/spec/migrations/add_premium_and_ultimate_plan_limits_spec.rb
index 0ae4559ca9f..670541128a0 100644
--- a/spec/migrations/add_premium_and_ultimate_plan_limits_spec.rb
+++ b/spec/migrations/add_premium_and_ultimate_plan_limits_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddPremiumAndUltimatePlanLimits, :migration do
+RSpec.describe AddPremiumAndUltimatePlanLimits, :migration, feature_category: :purchase do
shared_examples_for 'a migration that does not alter plans or plan limits' do
it do
expect { migrate! }.not_to change {
diff --git a/spec/migrations/add_triggers_to_integrations_type_new_spec.rb b/spec/migrations/add_triggers_to_integrations_type_new_spec.rb
index 01af5884170..4fa5fe31d2b 100644
--- a/spec/migrations/add_triggers_to_integrations_type_new_spec.rb
+++ b/spec/migrations/add_triggers_to_integrations_type_new_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddTriggersToIntegrationsTypeNew do
+RSpec.describe AddTriggersToIntegrationsTypeNew, feature_category: :purchase do
let(:migration) { described_class.new }
let(:integrations) { table(:integrations) }
diff --git a/spec/migrations/add_upvotes_count_index_to_issues_spec.rb b/spec/migrations/add_upvotes_count_index_to_issues_spec.rb
index c04cb98a107..0012b8a0b96 100644
--- a/spec/migrations/add_upvotes_count_index_to_issues_spec.rb
+++ b/spec/migrations/add_upvotes_count_index_to_issues_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe AddUpvotesCountIndexToIssues do
+RSpec.describe AddUpvotesCountIndexToIssues, feature_category: :team_planning do
let(:migration_instance) { described_class.new }
describe '#up' do
diff --git a/spec/migrations/add_web_hook_calls_to_plan_limits_paid_tiers_spec.rb b/spec/migrations/add_web_hook_calls_to_plan_limits_paid_tiers_spec.rb
index 63ad9367503..0ad99be1c7b 100644
--- a/spec/migrations/add_web_hook_calls_to_plan_limits_paid_tiers_spec.rb
+++ b/spec/migrations/add_web_hook_calls_to_plan_limits_paid_tiers_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
require_migration!
-RSpec.describe AddWebHookCallsToPlanLimitsPaidTiers do
- let_it_be(:plans) { table(:plans) }
- let_it_be(:plan_limits) { table(:plan_limits) }
+RSpec.describe AddWebHookCallsToPlanLimitsPaidTiers, feature_category: :purchase do
+ let!(:plans) { table(:plans) }
+ let!(:plan_limits) { table(:plan_limits) }
context 'when on Gitlab.com' do
let(:free_plan) { plans.create!(name: 'free') }
diff --git a/spec/migrations/adjust_task_note_rename_background_migration_values_spec.rb b/spec/migrations/adjust_task_note_rename_background_migration_values_spec.rb
index 422d0655e36..01680fa12cc 100644
--- a/spec/migrations/adjust_task_note_rename_background_migration_values_spec.rb
+++ b/spec/migrations/adjust_task_note_rename_background_migration_values_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe AdjustTaskNoteRenameBackgroundMigrationValues, :migration do
+RSpec.describe AdjustTaskNoteRenameBackgroundMigrationValues, :migration, feature_category: :team_planning do
let(:finished_status) { 3 }
let(:failed_status) { described_class::MIGRATION_FAILED_STATUS }
let(:active_status) { described_class::MIGRATION_ACTIVE_STATUS }
diff --git a/spec/migrations/associate_existing_dast_builds_with_variables_spec.rb b/spec/migrations/associate_existing_dast_builds_with_variables_spec.rb
index dd86989912f..67d215c781b 100644
--- a/spec/migrations/associate_existing_dast_builds_with_variables_spec.rb
+++ b/spec/migrations/associate_existing_dast_builds_with_variables_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe AssociateExistingDastBuildsWithVariables do
+RSpec.describe AssociateExistingDastBuildsWithVariables, feature_category: :dynamic_application_security_testing do
it 'is a no-op' do
migrate!
end
diff --git a/spec/migrations/backfill_all_project_namespaces_spec.rb b/spec/migrations/backfill_all_project_namespaces_spec.rb
index 1bcaad783b2..52fa46eea57 100644
--- a/spec/migrations/backfill_all_project_namespaces_spec.rb
+++ b/spec/migrations/backfill_all_project_namespaces_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillAllProjectNamespaces, :migration do
- let_it_be(:migration) { described_class::MIGRATION }
+RSpec.describe BackfillAllProjectNamespaces, :migration, feature_category: :subgroups do
+ let!(:migration) { described_class::MIGRATION }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/backfill_cadence_id_for_boards_scoped_to_iteration_spec.rb b/spec/migrations/backfill_cadence_id_for_boards_scoped_to_iteration_spec.rb
index 16a08ec47c4..a9500b9f942 100644
--- a/spec/migrations/backfill_cadence_id_for_boards_scoped_to_iteration_spec.rb
+++ b/spec/migrations/backfill_cadence_id_for_boards_scoped_to_iteration_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillCadenceIdForBoardsScopedToIteration, :migration do
+RSpec.describe BackfillCadenceIdForBoardsScopedToIteration, :migration, feature_category: :team_planning do
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
let(:iterations_cadences) { table(:iterations_cadences) }
diff --git a/spec/migrations/backfill_clusters_integration_prometheus_enabled_spec.rb b/spec/migrations/backfill_clusters_integration_prometheus_enabled_spec.rb
index 6c116120f05..1c7745a64ef 100644
--- a/spec/migrations/backfill_clusters_integration_prometheus_enabled_spec.rb
+++ b/spec/migrations/backfill_clusters_integration_prometheus_enabled_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillClustersIntegrationPrometheusEnabled, :migration do
+RSpec.describe BackfillClustersIntegrationPrometheusEnabled, :migration, feature_category: :clusters_applications_prometheus do
def create_cluster!(label = rand(2**64).to_s)
table(:clusters).create!(
name: "cluster: #{label}",
diff --git a/spec/migrations/backfill_cycle_analytics_aggregations_spec.rb b/spec/migrations/backfill_cycle_analytics_aggregations_spec.rb
index 2a5d33742ce..47950f918c3 100644
--- a/spec/migrations/backfill_cycle_analytics_aggregations_spec.rb
+++ b/spec/migrations/backfill_cycle_analytics_aggregations_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillCycleAnalyticsAggregations, :migration do
+RSpec.describe BackfillCycleAnalyticsAggregations, :migration, feature_category: :value_stream_management do
let(:migration) { described_class.new }
let(:aggregations) { table(:analytics_cycle_analytics_aggregations) }
diff --git a/spec/migrations/backfill_epic_cache_counts_spec.rb b/spec/migrations/backfill_epic_cache_counts_spec.rb
index 6084fdad0a6..1dc0079bb01 100644
--- a/spec/migrations/backfill_epic_cache_counts_spec.rb
+++ b/spec/migrations/backfill_epic_cache_counts_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillEpicCacheCounts, :migration do
+RSpec.describe BackfillEpicCacheCounts, :migration, feature_category: :portfolio_management do
let(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb b/spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb
index 87855285203..aa77a5c228a 100644
--- a/spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb
+++ b/spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb
@@ -3,48 +3,48 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillEscalationPoliciesForOncallSchedules do
- let_it_be(:projects) { table(:projects) }
- let_it_be(:schedules) { table(:incident_management_oncall_schedules) }
- let_it_be(:policies) { table(:incident_management_escalation_policies) }
- let_it_be(:rules) { table(:incident_management_escalation_rules) }
+RSpec.describe BackfillEscalationPoliciesForOncallSchedules, feature_category: :incident_management do
+ let!(:projects) { table(:projects) }
+ let!(:schedules) { table(:incident_management_oncall_schedules) }
+ let!(:policies) { table(:incident_management_escalation_policies) }
+ let!(:rules) { table(:incident_management_escalation_rules) }
# Project with no schedules
- let_it_be(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') }
- let_it_be(:project_a) { projects.create!(namespace_id: namespace.id) }
+ let!(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') }
+ let!(:project_a) { projects.create!(namespace_id: namespace.id) }
context 'with backfill-able schedules' do
# Project with one schedule
- let_it_be(:project_b) { projects.create!(namespace_id: namespace.id) }
- let_it_be(:schedule_b1) { schedules.create!(project_id: project_b.id, iid: 1, name: 'Schedule B1') }
+ let!(:project_b) { projects.create!(namespace_id: namespace.id) }
+ let!(:schedule_b1) { schedules.create!(project_id: project_b.id, iid: 1, name: 'Schedule B1') }
# Project with multiple schedules
- let_it_be(:project_c) { projects.create!(namespace_id: namespace.id) }
- let_it_be(:schedule_c1) { schedules.create!(project_id: project_c.id, iid: 1, name: 'Schedule C1') }
- let_it_be(:schedule_c2) { schedules.create!(project_id: project_c.id, iid: 2, name: 'Schedule C2') }
+ let!(:project_c) { projects.create!(namespace_id: namespace.id) }
+ let!(:schedule_c1) { schedules.create!(project_id: project_c.id, iid: 1, name: 'Schedule C1') }
+ let!(:schedule_c2) { schedules.create!(project_id: project_c.id, iid: 2, name: 'Schedule C2') }
# Project with a single schedule which already has a policy
- let_it_be(:project_d) { projects.create!(namespace_id: namespace.id) }
- let_it_be(:schedule_d1) { schedules.create!(project_id: project_d.id, iid: 1, name: 'Schedule D1') }
- let_it_be(:policy_d1) { policies.create!(project_id: project_d.id, name: 'Policy D1') }
- let_it_be(:rule_d1) { rules.create!(policy_id: policy_d1.id, oncall_schedule_id: schedule_d1.id, status: 2, elapsed_time_seconds: 60) }
+ let!(:project_d) { projects.create!(namespace_id: namespace.id) }
+ let!(:schedule_d1) { schedules.create!(project_id: project_d.id, iid: 1, name: 'Schedule D1') }
+ let!(:policy_d1) { policies.create!(project_id: project_d.id, name: 'Policy D1') }
+ let!(:rule_d1) { rules.create!(policy_id: policy_d1.id, oncall_schedule_id: schedule_d1.id, status: 2, elapsed_time_seconds: 60) }
# Project with a multiple schedule, one of which already has a policy
- let_it_be(:project_e) { projects.create!(namespace_id: namespace.id) }
- let_it_be(:schedule_e1) { schedules.create!(project_id: project_e.id, iid: 1, name: 'Schedule E1') }
- let_it_be(:schedule_e2) { schedules.create!(project_id: project_e.id, iid: 2, name: 'Schedule E2') }
- let_it_be(:policy_e1) { policies.create!(project_id: project_e.id, name: 'Policy E1') }
- let_it_be(:rule_e1) { rules.create!(policy_id: policy_e1.id, oncall_schedule_id: schedule_e2.id, status: 2, elapsed_time_seconds: 60) }
+ let!(:project_e) { projects.create!(namespace_id: namespace.id) }
+ let!(:schedule_e1) { schedules.create!(project_id: project_e.id, iid: 1, name: 'Schedule E1') }
+ let!(:schedule_e2) { schedules.create!(project_id: project_e.id, iid: 2, name: 'Schedule E2') }
+ let!(:policy_e1) { policies.create!(project_id: project_e.id, name: 'Policy E1') }
+ let!(:rule_e1) { rules.create!(policy_id: policy_e1.id, oncall_schedule_id: schedule_e2.id, status: 2, elapsed_time_seconds: 60) }
# Project with a multiple schedule, with multiple policies
- let_it_be(:project_f) { projects.create!(namespace_id: namespace.id) }
- let_it_be(:schedule_f1) { schedules.create!(project_id: project_f.id, iid: 1, name: 'Schedule F1') }
- let_it_be(:schedule_f2) { schedules.create!(project_id: project_f.id, iid: 2, name: 'Schedule F2') }
- let_it_be(:policy_f1) { policies.create!(project_id: project_f.id, name: 'Policy F1') }
- let_it_be(:rule_f1) { rules.create!(policy_id: policy_f1.id, oncall_schedule_id: schedule_f1.id, status: 2, elapsed_time_seconds: 60) }
- let_it_be(:rule_f2) { rules.create!(policy_id: policy_f1.id, oncall_schedule_id: schedule_f2.id, status: 2, elapsed_time_seconds: 60) }
- let_it_be(:policy_f2) { policies.create!(project_id: project_f.id, name: 'Policy F2') }
- let_it_be(:rule_f3) { rules.create!(policy_id: policy_f2.id, oncall_schedule_id: schedule_f2.id, status: 1, elapsed_time_seconds: 10) }
+ let!(:project_f) { projects.create!(namespace_id: namespace.id) }
+ let!(:schedule_f1) { schedules.create!(project_id: project_f.id, iid: 1, name: 'Schedule F1') }
+ let!(:schedule_f2) { schedules.create!(project_id: project_f.id, iid: 2, name: 'Schedule F2') }
+ let!(:policy_f1) { policies.create!(project_id: project_f.id, name: 'Policy F1') }
+ let!(:rule_f1) { rules.create!(policy_id: policy_f1.id, oncall_schedule_id: schedule_f1.id, status: 2, elapsed_time_seconds: 60) }
+ let!(:rule_f2) { rules.create!(policy_id: policy_f1.id, oncall_schedule_id: schedule_f2.id, status: 2, elapsed_time_seconds: 60) }
+ let!(:policy_f2) { policies.create!(project_id: project_f.id, name: 'Policy F2') }
+ let!(:rule_f3) { rules.create!(policy_id: policy_f2.id, oncall_schedule_id: schedule_f2.id, status: 1, elapsed_time_seconds: 10) }
it 'backfills escalation policies correctly' do
expect { migrate! }
diff --git a/spec/migrations/backfill_group_features_spec.rb b/spec/migrations/backfill_group_features_spec.rb
index 922d54f43be..1e7729a97d8 100644
--- a/spec/migrations/backfill_group_features_spec.rb
+++ b/spec/migrations/backfill_group_features_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillGroupFeatures, :migration do
+RSpec.describe BackfillGroupFeatures, :migration, feature_category: :feature_flags do
let(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb b/spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb
index 28578a3d79a..5029a861d31 100644
--- a/spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb
+++ b/spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillIntegrationsEnableSslVerification do
- let_it_be(:migration) { described_class::MIGRATION }
- let_it_be(:integrations) { described_class::Integration }
+RSpec.describe BackfillIntegrationsEnableSslVerification, feature_category: :authentication_and_authorization do
+ let!(:migration) { described_class::MIGRATION }
+ let!(:integrations) { described_class::Integration }
before do
stub_const("#{described_class.name}::BATCH_SIZE", 2)
diff --git a/spec/migrations/backfill_integrations_type_new_spec.rb b/spec/migrations/backfill_integrations_type_new_spec.rb
index 5b8fbf6f555..79519c4439a 100644
--- a/spec/migrations/backfill_integrations_type_new_spec.rb
+++ b/spec/migrations/backfill_integrations_type_new_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillIntegrationsTypeNew do
- let_it_be(:migration) { described_class::MIGRATION }
- let_it_be(:integrations) { table(:integrations) }
+RSpec.describe BackfillIntegrationsTypeNew, feature_category: :integrations do
+ let!(:migration) { described_class::MIGRATION }
+ let!(:integrations) { table(:integrations) }
before do
integrations.create!(id: 1)
diff --git a/spec/migrations/backfill_issues_upvotes_count_spec.rb b/spec/migrations/backfill_issues_upvotes_count_spec.rb
index 94cfa29ae89..b8687595b35 100644
--- a/spec/migrations/backfill_issues_upvotes_count_spec.rb
+++ b/spec/migrations/backfill_issues_upvotes_count_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillIssuesUpvotesCount do
+RSpec.describe BackfillIssuesUpvotesCount, feature_category: :team_planning do
let(:migration) { described_class.new }
let(:issues) { table(:issues) }
let(:award_emoji) { table(:award_emoji) }
diff --git a/spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb b/spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb
index 0c0acf85d41..892589dd770 100644
--- a/spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb
+++ b/spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillMemberNamespaceIdForGroupMembers do
- let_it_be(:migration) { described_class::MIGRATION }
+RSpec.describe BackfillMemberNamespaceIdForGroupMembers, feature_category: :subgroups do
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of group members' do
diff --git a/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb b/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb
index 913ec404795..627b18cd889 100644
--- a/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb
+++ b/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillNamespaceIdForNamespaceRoutes do
- let_it_be(:migration) { described_class::MIGRATION }
+RSpec.describe BackfillNamespaceIdForNamespaceRoutes, feature_category: :projects do
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of routes' do
diff --git a/spec/migrations/backfill_namespace_id_for_project_routes_spec.rb b/spec/migrations/backfill_namespace_id_for_project_routes_spec.rb
index 28edd17731f..773c1733a4a 100644
--- a/spec/migrations/backfill_namespace_id_for_project_routes_spec.rb
+++ b/spec/migrations/backfill_namespace_id_for_project_routes_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillNamespaceIdForProjectRoutes, :migration do
+RSpec.describe BackfillNamespaceIdForProjectRoutes, :migration, feature_category: :subgroups do
let(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/backfill_namespace_id_on_issues_spec.rb b/spec/migrations/backfill_namespace_id_on_issues_spec.rb
index 2721d7ce8f1..28453394cb0 100644
--- a/spec/migrations/backfill_namespace_id_on_issues_spec.rb
+++ b/spec/migrations/backfill_namespace_id_on_issues_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillNamespaceIdOnIssues, :migration do
+RSpec.describe BackfillNamespaceIdOnIssues, :migration, feature_category: :team_planning do
let(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/backfill_nuget_temporary_packages_to_processing_status_spec.rb b/spec/migrations/backfill_nuget_temporary_packages_to_processing_status_spec.rb
index 574020e52d5..ae2656eaf98 100644
--- a/spec/migrations/backfill_nuget_temporary_packages_to_processing_status_spec.rb
+++ b/spec/migrations/backfill_nuget_temporary_packages_to_processing_status_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe BackfillNugetTemporaryPackagesToProcessingStatus, :migration do
+RSpec.describe BackfillNugetTemporaryPackagesToProcessingStatus, :migration, feature_category: :package_registry do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:packages) { table(:packages_packages) }
diff --git a/spec/migrations/backfill_project_import_level_spec.rb b/spec/migrations/backfill_project_import_level_spec.rb
index c24ddac0730..b41e323a92f 100644
--- a/spec/migrations/backfill_project_import_level_spec.rb
+++ b/spec/migrations/backfill_project_import_level_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillProjectImportLevel do
- let_it_be(:batched_migration) { described_class::MIGRATION }
+RSpec.describe BackfillProjectImportLevel, feature_category: :importers do
+ let!(:batched_migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of namespaces' do
diff --git a/spec/migrations/backfill_project_namespaces_for_group_spec.rb b/spec/migrations/backfill_project_namespaces_for_group_spec.rb
index 0d34d19d42a..b21ed6e1aa2 100644
--- a/spec/migrations/backfill_project_namespaces_for_group_spec.rb
+++ b/spec/migrations/backfill_project_namespaces_for_group_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillProjectNamespacesForGroup do
- let_it_be(:migration) { described_class::MIGRATION }
+RSpec.describe BackfillProjectNamespacesForGroup, feature_category: :subgroups do
+ let!(:migration) { described_class::MIGRATION }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/backfill_stage_event_hash_spec.rb b/spec/migrations/backfill_stage_event_hash_spec.rb
index cecaddcd3d4..399a9c4dfde 100644
--- a/spec/migrations/backfill_stage_event_hash_spec.rb
+++ b/spec/migrations/backfill_stage_event_hash_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe BackfillStageEventHash, schema: 20210730103808 do
+RSpec.describe BackfillStageEventHash, schema: 20210730103808, feature_category: :value_stream_management do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:labels) { table(:labels) }
diff --git a/spec/migrations/backfill_user_namespace_spec.rb b/spec/migrations/backfill_user_namespace_spec.rb
index 094aec82e9c..a58030803b1 100644
--- a/spec/migrations/backfill_user_namespace_spec.rb
+++ b/spec/migrations/backfill_user_namespace_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillUserNamespace do
- let_it_be(:migration) { described_class::MIGRATION }
+RSpec.describe BackfillUserNamespace, feature_category: :subgroups do
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of namespaces' do
diff --git a/spec/migrations/bulk_insert_cluster_enabled_grants_spec.rb b/spec/migrations/bulk_insert_cluster_enabled_grants_spec.rb
index a359a78ab45..e85489198ee 100644
--- a/spec/migrations/bulk_insert_cluster_enabled_grants_spec.rb
+++ b/spec/migrations/bulk_insert_cluster_enabled_grants_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe BulkInsertClusterEnabledGrants, :migration do
+RSpec.describe BulkInsertClusterEnabledGrants, :migration, feature_category: :kubernetes_management do
let(:migration) { described_class.new }
let(:cluster_enabled_grants) { table(:cluster_enabled_grants) }
diff --git a/spec/migrations/change_public_projects_cost_factor_spec.rb b/spec/migrations/change_public_projects_cost_factor_spec.rb
index 039edda750b..656c8a45c57 100644
--- a/spec/migrations/change_public_projects_cost_factor_spec.rb
+++ b/spec/migrations/change_public_projects_cost_factor_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ChangePublicProjectsCostFactor, migration: :gitlab_ci do
+RSpec.describe ChangePublicProjectsCostFactor, migration: :gitlab_ci, feature_category: :runner do
let(:runners) { table(:ci_runners) }
let!(:shared_1) { runners.create!(runner_type: 1, public_projects_minutes_cost_factor: 0) }
diff --git a/spec/migrations/change_task_system_note_wording_to_checklist_item_spec.rb b/spec/migrations/change_task_system_note_wording_to_checklist_item_spec.rb
index 039ee92f8bd..421c519b2bc 100644
--- a/spec/migrations/change_task_system_note_wording_to_checklist_item_spec.rb
+++ b/spec/migrations/change_task_system_note_wording_to_checklist_item_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ChangeTaskSystemNoteWordingToChecklistItem, :migration do
+RSpec.describe ChangeTaskSystemNoteWordingToChecklistItem, :migration, feature_category: :team_planning do
let(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/change_web_hook_events_default_spec.rb b/spec/migrations/change_web_hook_events_default_spec.rb
index aad187187d0..c6c3f285ff1 100644
--- a/spec/migrations/change_web_hook_events_default_spec.rb
+++ b/spec/migrations/change_web_hook_events_default_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ChangeWebHookEventsDefault do
+RSpec.describe ChangeWebHookEventsDefault, feature_category: :integrations do
let(:web_hooks) { table(:web_hooks) }
let(:projects) { table(:projects) }
let(:groups) { table(:namespaces) }
diff --git a/spec/migrations/clean_up_pending_builds_table_spec.rb b/spec/migrations/clean_up_pending_builds_table_spec.rb
index 17e62e1b486..e044d4a702b 100644
--- a/spec/migrations/clean_up_pending_builds_table_spec.rb
+++ b/spec/migrations/clean_up_pending_builds_table_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanUpPendingBuildsTable, :suppress_gitlab_schemas_validate_connection do
+RSpec.describe CleanUpPendingBuildsTable, :suppress_gitlab_schemas_validate_connection,
+feature_category: :continuous_integration do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:queue) { table(:ci_pending_builds) }
diff --git a/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb b/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb
index abff7c6aba1..6027199c11c 100644
--- a/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb
+++ b/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupAfterAddPrimaryEmailToEmailsIfUserConfirmed, :sidekiq do
+RSpec.describe CleanupAfterAddPrimaryEmailToEmailsIfUserConfirmed, :sidekiq, feature_category: :users do
let(:migration) { described_class.new }
let(:users) { table(:users) }
let(:emails) { table(:emails) }
diff --git a/spec/migrations/cleanup_after_fixing_issue_when_admin_changed_primary_email_spec.rb b/spec/migrations/cleanup_after_fixing_issue_when_admin_changed_primary_email_spec.rb
index eda57545c7a..e8dce46bdbc 100644
--- a/spec/migrations/cleanup_after_fixing_issue_when_admin_changed_primary_email_spec.rb
+++ b/spec/migrations/cleanup_after_fixing_issue_when_admin_changed_primary_email_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupAfterFixingIssueWhenAdminChangedPrimaryEmail, :sidekiq do
+RSpec.describe CleanupAfterFixingIssueWhenAdminChangedPrimaryEmail, :sidekiq, feature_category: :users do
let(:migration) { described_class.new }
let(:users) { table(:users) }
let(:emails) { table(:emails) }
diff --git a/spec/migrations/cleanup_after_fixing_regression_with_new_users_emails_spec.rb b/spec/migrations/cleanup_after_fixing_regression_with_new_users_emails_spec.rb
index 043bb091df3..01ceef9f3a1 100644
--- a/spec/migrations/cleanup_after_fixing_regression_with_new_users_emails_spec.rb
+++ b/spec/migrations/cleanup_after_fixing_regression_with_new_users_emails_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupAfterFixingRegressionWithNewUsersEmails, :sidekiq do
+RSpec.describe CleanupAfterFixingRegressionWithNewUsersEmails, :sidekiq, feature_category: :users do
let(:migration) { described_class.new }
let(:users) { table(:users) }
let(:emails) { table(:emails) }
diff --git a/spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb b/spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb
index 1517405b358..7aaa90ee985 100644
--- a/spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb
+++ b/spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupBackfillIntegrationsEnableSslVerification, :migration do
+RSpec.describe CleanupBackfillIntegrationsEnableSslVerification, :migration,
+feature_category: :authentication_and_authorization do
let(:job_class_name) { 'BackfillIntegrationsEnableSslVerification' }
before do
diff --git a/spec/migrations/cleanup_move_container_registry_enabled_to_project_feature_spec.rb b/spec/migrations/cleanup_move_container_registry_enabled_to_project_feature_spec.rb
index f0f9249515b..1badde62526 100644
--- a/spec/migrations/cleanup_move_container_registry_enabled_to_project_feature_spec.rb
+++ b/spec/migrations/cleanup_move_container_registry_enabled_to_project_feature_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupMoveContainerRegistryEnabledToProjectFeature, :migration do
+RSpec.describe CleanupMoveContainerRegistryEnabledToProjectFeature, :migration, feature_category: :navigation do
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
let(:non_null_project_features) { { pages_access_level: 20 } }
let(:bg_class_name) { 'MoveContainerRegistryEnabledToProjectFeature' }
diff --git a/spec/migrations/cleanup_mr_attention_request_todos_spec.rb b/spec/migrations/cleanup_mr_attention_request_todos_spec.rb
index 9f593ca8292..4fa2419aa7c 100644
--- a/spec/migrations/cleanup_mr_attention_request_todos_spec.rb
+++ b/spec/migrations/cleanup_mr_attention_request_todos_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupMrAttentionRequestTodos, :migration do
+RSpec.describe CleanupMrAttentionRequestTodos, :migration, feature_category: :code_review do
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
let(:users) { table(:users) }
diff --git a/spec/migrations/cleanup_orphaned_routes_spec.rb b/spec/migrations/cleanup_orphaned_routes_spec.rb
index 68598939557..a0ce9062c70 100644
--- a/spec/migrations/cleanup_orphaned_routes_spec.rb
+++ b/spec/migrations/cleanup_orphaned_routes_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupOrphanedRoutes, :migration do
+RSpec.describe CleanupOrphanedRoutes, :migration, feature_category: :projects do
let(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/cleanup_remaining_orphan_invites_spec.rb b/spec/migrations/cleanup_remaining_orphan_invites_spec.rb
index 987535a4f09..598030c99a0 100644
--- a/spec/migrations/cleanup_remaining_orphan_invites_spec.rb
+++ b/spec/migrations/cleanup_remaining_orphan_invites_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupRemainingOrphanInvites, :migration do
+RSpec.describe CleanupRemainingOrphanInvites, :migration, feature_category: :subgroups do
def create_member(**extra_attributes)
defaults = {
access_level: 10,
diff --git a/spec/migrations/cleanup_vulnerability_state_transitions_with_same_from_state_to_state_spec.rb b/spec/migrations/cleanup_vulnerability_state_transitions_with_same_from_state_to_state_spec.rb
index 92ece81ffc8..b808f03428d 100644
--- a/spec/migrations/cleanup_vulnerability_state_transitions_with_same_from_state_to_state_spec.rb
+++ b/spec/migrations/cleanup_vulnerability_state_transitions_with_same_from_state_to_state_spec.rb
@@ -3,10 +3,11 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupVulnerabilityStateTransitionsWithSameFromStateToState, :migration do
- let_it_be(:namespace) { table(:namespaces).create!(name: 'namespace', type: 'Group', path: 'namespace') }
- let_it_be(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) }
- let_it_be(:project) do
+RSpec.describe CleanupVulnerabilityStateTransitionsWithSameFromStateToState, :migration,
+feature_category: :vulnerability_management do
+ let!(:namespace) { table(:namespaces).create!(name: 'namespace', type: 'Group', path: 'namespace') }
+ let!(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let!(:project) do
table(:projects).create!(
path: 'project',
namespace_id: namespace.id,
@@ -14,7 +15,7 @@ RSpec.describe CleanupVulnerabilityStateTransitionsWithSameFromStateToState, :mi
)
end
- let_it_be(:vulnerability) do
+ let!(:vulnerability) do
table(:vulnerabilities).create!(
project_id: project.id,
author_id: user.id,
@@ -25,7 +26,7 @@ RSpec.describe CleanupVulnerabilityStateTransitionsWithSameFromStateToState, :mi
)
end
- let_it_be(:state_transitions) { table(:vulnerability_state_transitions) }
+ let!(:state_transitions) { table(:vulnerability_state_transitions) }
let!(:state_transition_with_no_state_change) do
state_transitions.create!(
diff --git a/spec/migrations/confirm_security_bot_spec.rb b/spec/migrations/confirm_security_bot_spec.rb
index 19ca81f92f3..3761c113692 100644
--- a/spec/migrations/confirm_security_bot_spec.rb
+++ b/spec/migrations/confirm_security_bot_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ConfirmSecurityBot, :migration do
+RSpec.describe ConfirmSecurityBot, :migration, feature_category: :users do
let(:users) { table(:users) }
let(:user_type) { 8 }
diff --git a/spec/migrations/confirm_support_bot_user_spec.rb b/spec/migrations/confirm_support_bot_user_spec.rb
index c60c7fe45f7..863bdb13585 100644
--- a/spec/migrations/confirm_support_bot_user_spec.rb
+++ b/spec/migrations/confirm_support_bot_user_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ConfirmSupportBotUser, :migration do
+RSpec.describe ConfirmSupportBotUser, :migration, feature_category: :users do
let(:users) { table(:users) }
context 'when support bot user is currently unconfirmed' do
@@ -72,7 +72,7 @@ RSpec.describe ConfirmSupportBotUser, :migration do
private
- def create_user!(name: 'GitLab Support Bot', email: 'support@example.com', user_type:, created_at: Time.now, confirmed_at: nil, record_timestamps: true)
+ def create_user!(user_type:, name: 'GitLab Support Bot', email: 'support@example.com', created_at: Time.now, confirmed_at: nil, record_timestamps: true)
users.create!(
name: name,
email: email,
diff --git a/spec/migrations/delete_migrate_shared_vulnerability_scanners_spec.rb b/spec/migrations/delete_migrate_shared_vulnerability_scanners_spec.rb
index 259b175cd19..562b1e25db4 100644
--- a/spec/migrations/delete_migrate_shared_vulnerability_scanners_spec.rb
+++ b/spec/migrations/delete_migrate_shared_vulnerability_scanners_spec.rb
@@ -4,7 +4,7 @@ require "spec_helper"
require_migration!
-RSpec.describe DeleteMigrateSharedVulnerabilityScanners, :migration do
+RSpec.describe DeleteMigrateSharedVulnerabilityScanners, :migration, feature_category: :vulnerability_management do
let(:batched_background_migrations) { table(:batched_background_migrations) }
let(:batched_background_migration_jobs) { table(:batched_background_migration_jobs) }
diff --git a/spec/migrations/delete_security_findings_without_uuid_spec.rb b/spec/migrations/delete_security_findings_without_uuid_spec.rb
index bfd89f1aa82..e4c17288384 100644
--- a/spec/migrations/delete_security_findings_without_uuid_spec.rb
+++ b/spec/migrations/delete_security_findings_without_uuid_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe DeleteSecurityFindingsWithoutUuid, :suppress_gitlab_schemas_validate_connection do
+RSpec.describe DeleteSecurityFindingsWithoutUuid, :suppress_gitlab_schemas_validate_connection,
+feature_category: :vulnerability_management do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/disable_expiration_policies_linked_to_no_container_images_spec.rb b/spec/migrations/disable_expiration_policies_linked_to_no_container_images_spec.rb
index f2be06f1ed6..1d948257fcc 100644
--- a/spec/migrations/disable_expiration_policies_linked_to_no_container_images_spec.rb
+++ b/spec/migrations/disable_expiration_policies_linked_to_no_container_images_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe DisableExpirationPoliciesLinkedToNoContainerImages do
+RSpec.describe DisableExpirationPoliciesLinkedToNoContainerImages, feature_category: :container_registry do
let(:projects) { table(:projects) }
let(:container_expiration_policies) { table(:container_expiration_policies) }
let(:container_repositories) { table(:container_repositories) }
diff --git a/spec/migrations/disable_job_token_scope_when_unused_spec.rb b/spec/migrations/disable_job_token_scope_when_unused_spec.rb
index 3ce4ef5c102..fddf3594e2b 100644
--- a/spec/migrations/disable_job_token_scope_when_unused_spec.rb
+++ b/spec/migrations/disable_job_token_scope_when_unused_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe DisableJobTokenScopeWhenUnused do
+RSpec.describe DisableJobTokenScopeWhenUnused, feature_category: :continuous_integration do
it 'is a no-op' do
migrate!
end
diff --git a/spec/migrations/finalize_invalid_member_cleanup_spec.rb b/spec/migrations/finalize_invalid_member_cleanup_spec.rb
index a29a89c2396..29d03f8983c 100644
--- a/spec/migrations/finalize_invalid_member_cleanup_spec.rb
+++ b/spec/migrations/finalize_invalid_member_cleanup_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe FinalizeInvalidMemberCleanup, :migration do
+RSpec.describe FinalizeInvalidMemberCleanup, :migration, feature_category: :subgroups do
let(:batched_migrations) { table(:batched_background_migrations) }
- let_it_be(:migration) { described_class::MIGRATION }
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
shared_examples 'finalizes the migration' do
diff --git a/spec/migrations/finalize_issues_namespace_id_backfilling_spec.rb b/spec/migrations/finalize_issues_namespace_id_backfilling_spec.rb
new file mode 100644
index 00000000000..d0c25fb3dd6
--- /dev/null
+++ b/spec/migrations/finalize_issues_namespace_id_backfilling_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FinalizeIssuesNamespaceIdBackfilling, :migration, feature_category: :team_planning do
+ let(:batched_migrations) { table(:batched_background_migrations) }
+
+ let!(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ shared_examples 'finalizes the migration' do
+ it 'finalizes the migration' do
+ allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
+ expect(runner).to receive(:finalize).with('BackfillProjectNamespaceOnIssues', :projects, :id, [])
+ end
+ end
+ end
+
+ context 'when routes backfilling migration is missing' do
+ it 'warns migration not found' do
+ expect(Gitlab::AppLogger)
+ .to receive(:warn).with(/Could not find batched background migration for the given configuration:/)
+
+ migrate!
+ end
+ end
+
+ context 'with backfilling migration present' do
+ let!(:project_namespace_backfill) do
+ batched_migrations.create!(
+ job_class_name: 'BackfillProjectNamespaceOnIssues',
+ table_name: :routes,
+ column_name: :id,
+ job_arguments: [],
+ interval: 2.minutes,
+ min_value: 1,
+ max_value: 2,
+ batch_size: 1000,
+ sub_batch_size: 200,
+ gitlab_schema: :gitlab_main,
+ status: 3 # finished
+ )
+ end
+
+ context 'when backfilling migration finished successfully' do
+ it 'does not raise exception' do
+ expect { migrate! }.not_to raise_error
+ end
+ end
+
+ context 'with different backfilling migration statuses' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :description) do
+ 0 | 'paused'
+ 1 | 'active'
+ 4 | 'failed'
+ 5 | 'finalizing'
+ end
+
+ with_them do
+ before do
+ project_namespace_backfill.update!(status: status)
+ end
+
+ it_behaves_like 'finalizes the migration'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/finalize_orphaned_routes_cleanup_spec.rb b/spec/migrations/finalize_orphaned_routes_cleanup_spec.rb
index dfc95ed9e63..78546806039 100644
--- a/spec/migrations/finalize_orphaned_routes_cleanup_spec.rb
+++ b/spec/migrations/finalize_orphaned_routes_cleanup_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe FinalizeOrphanedRoutesCleanup, :migration do
+RSpec.describe FinalizeOrphanedRoutesCleanup, :migration, feature_category: :projects do
let(:batched_migrations) { table(:batched_background_migrations) }
- let_it_be(:migration) { described_class::MIGRATION }
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
shared_examples 'finalizes the migration' do
diff --git a/spec/migrations/finalize_project_namespaces_backfill_spec.rb b/spec/migrations/finalize_project_namespaces_backfill_spec.rb
index 56f3b0f6ba5..6cc3a694de8 100644
--- a/spec/migrations/finalize_project_namespaces_backfill_spec.rb
+++ b/spec/migrations/finalize_project_namespaces_backfill_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe FinalizeProjectNamespacesBackfill, :migration do
+RSpec.describe FinalizeProjectNamespacesBackfill, :migration, feature_category: :projects do
let(:batched_migrations) { table(:batched_background_migrations) }
- let_it_be(:migration) { described_class::MIGRATION }
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
shared_examples 'finalizes the migration' do
diff --git a/spec/migrations/finalize_routes_backfilling_for_projects_spec.rb b/spec/migrations/finalize_routes_backfilling_for_projects_spec.rb
index 2bb740d0c2f..b79fdc98425 100644
--- a/spec/migrations/finalize_routes_backfilling_for_projects_spec.rb
+++ b/spec/migrations/finalize_routes_backfilling_for_projects_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe FinalizeRoutesBackfillingForProjects, :migration do
+RSpec.describe FinalizeRoutesBackfillingForProjects, :migration, feature_category: :projects do
let(:batched_migrations) { table(:batched_background_migrations) }
- let_it_be(:migration) { described_class::MIGRATION }
+ let!(:migration) { described_class::MIGRATION }
describe '#up' do
shared_examples 'finalizes the migration' do
diff --git a/spec/migrations/finalize_traversal_ids_background_migrations_spec.rb b/spec/migrations/finalize_traversal_ids_background_migrations_spec.rb
index 74d6447e6a7..0cebe7b9f91 100644
--- a/spec/migrations/finalize_traversal_ids_background_migrations_spec.rb
+++ b/spec/migrations/finalize_traversal_ids_background_migrations_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!('finalize_traversal_ids_background_migrations')
-RSpec.describe FinalizeTraversalIdsBackgroundMigrations, :migration do
+RSpec.describe FinalizeTraversalIdsBackgroundMigrations, :migration, feature_category: :database do
shared_context 'incomplete background migration' do
before do
# Jobs enqueued in Sidekiq.
diff --git a/spec/migrations/fix_and_backfill_project_namespaces_for_projects_with_duplicate_name_spec.rb b/spec/migrations/fix_and_backfill_project_namespaces_for_projects_with_duplicate_name_spec.rb
index 44a2220b2ad..6b9fb1c6f2c 100644
--- a/spec/migrations/fix_and_backfill_project_namespaces_for_projects_with_duplicate_name_spec.rb
+++ b/spec/migrations/fix_and_backfill_project_namespaces_for_projects_with_duplicate_name_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe FixAndBackfillProjectNamespacesForProjectsWithDuplicateName, :migration do
+RSpec.describe FixAndBackfillProjectNamespacesForProjectsWithDuplicateName, :migration, feature_category: :projects do
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/fix_batched_migrations_old_format_job_arguments_spec.rb b/spec/migrations/fix_batched_migrations_old_format_job_arguments_spec.rb
index e15011d0dab..8def53e1858 100644
--- a/spec/migrations/fix_batched_migrations_old_format_job_arguments_spec.rb
+++ b/spec/migrations/fix_batched_migrations_old_format_job_arguments_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
# rubocop:disable Style/WordArray
-RSpec.describe FixBatchedMigrationsOldFormatJobArguments do
+RSpec.describe FixBatchedMigrationsOldFormatJobArguments, feature_category: :users do
let(:batched_background_migrations) { table(:batched_background_migrations) }
context 'when migrations with legacy job arguments exists' do
diff --git a/spec/migrations/generate_customers_dot_jwt_signing_key_spec.rb b/spec/migrations/generate_customers_dot_jwt_signing_key_spec.rb
index b7a91abf5d7..1385b67b607 100644
--- a/spec/migrations/generate_customers_dot_jwt_signing_key_spec.rb
+++ b/spec/migrations/generate_customers_dot_jwt_signing_key_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe GenerateCustomersDotJwtSigningKey do
+RSpec.describe GenerateCustomersDotJwtSigningKey, feature_category: :customersdot_application do
let(:application_settings) do
Class.new(ActiveRecord::Base) do
self.table_name = 'application_settings'
diff --git a/spec/migrations/insert_ci_daily_pipeline_schedule_triggers_plan_limits_spec.rb b/spec/migrations/insert_ci_daily_pipeline_schedule_triggers_plan_limits_spec.rb
index 1b6cb6a86a0..9358b71132c 100644
--- a/spec/migrations/insert_ci_daily_pipeline_schedule_triggers_plan_limits_spec.rb
+++ b/spec/migrations/insert_ci_daily_pipeline_schedule_triggers_plan_limits_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
require_migration!
-RSpec.describe InsertCiDailyPipelineScheduleTriggersPlanLimits do
- let_it_be(:plans) { table(:plans) }
- let_it_be(:plan_limits) { table(:plan_limits) }
+RSpec.describe InsertCiDailyPipelineScheduleTriggersPlanLimits, feature_category: :purchase do
+ let!(:plans) { table(:plans) }
+ let!(:plan_limits) { table(:plan_limits) }
context 'when on Gitlab.com' do
let(:free_plan) { plans.create!(name: 'free') }
@@ -46,25 +46,25 @@ RSpec.describe InsertCiDailyPipelineScheduleTriggersPlanLimits do
end
context 'when on self hosted' do
- let(:free_plan) { plans.create!(name: 'free') }
+ let(:default_plan) { plans.create!(name: 'default') }
before do
allow(Gitlab).to receive(:com?).and_return(false)
- plan_limits.create!(plan_id: free_plan.id)
+ plan_limits.create!(plan_id: default_plan.id)
end
it 'does nothing' do
reversible_migration do |migration|
migration.before -> {
expect(plan_limits.pluck(:plan_id, :ci_daily_pipeline_schedule_triggers)).to contain_exactly(
- [free_plan.id, 0]
+ [default_plan.id, 0]
)
}
migration.after -> {
expect(plan_limits.pluck(:plan_id, :ci_daily_pipeline_schedule_triggers)).to contain_exactly(
- [free_plan.id, 0]
+ [default_plan.id, 0]
)
}
end
diff --git a/spec/migrations/migrate_elastic_index_settings_spec.rb b/spec/migrations/migrate_elastic_index_settings_spec.rb
index 5f39d9b9fc1..b67c4d902c7 100644
--- a/spec/migrations/migrate_elastic_index_settings_spec.rb
+++ b/spec/migrations/migrate_elastic_index_settings_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe MigrateElasticIndexSettings do
+RSpec.describe MigrateElasticIndexSettings, feature_category: :global_search do
let(:elastic_index_settings) { table(:elastic_index_settings) }
let(:application_settings) { table(:application_settings) }
diff --git a/spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb b/spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb
index 01805a9eb79..2f62147da9d 100644
--- a/spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb
+++ b/spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe MigrateProtectedAttributeToPendingBuilds, :suppress_gitlab_schemas_validate_connection do
+RSpec.describe MigrateProtectedAttributeToPendingBuilds, :suppress_gitlab_schemas_validate_connection,
+feature_category: :continuous_integration do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:queue) { table(:ci_pending_builds) }
diff --git a/spec/migrations/move_container_registry_enabled_to_project_features3_spec.rb b/spec/migrations/move_container_registry_enabled_to_project_features3_spec.rb
index e47cea749d6..25e0ef439bd 100644
--- a/spec/migrations/move_container_registry_enabled_to_project_features3_spec.rb
+++ b/spec/migrations/move_container_registry_enabled_to_project_features3_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe MoveContainerRegistryEnabledToProjectFeatures3, :migration do
+RSpec.describe MoveContainerRegistryEnabledToProjectFeatures3, :migration, feature_category: :container_registry do
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
let!(:background_jobs) do
diff --git a/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb b/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb
index b5bb86edce2..2533d3224a6 100644
--- a/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb
+++ b/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema do
+RSpec.describe MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema, feature_category: :vulnerability_management do
let(:partitions_sql) do
<<~SQL
SELECT
diff --git a/spec/migrations/orphaned_invite_tokens_cleanup_spec.rb b/spec/migrations/orphaned_invite_tokens_cleanup_spec.rb
index b33e29f82e2..56f47fca864 100644
--- a/spec/migrations/orphaned_invite_tokens_cleanup_spec.rb
+++ b/spec/migrations/orphaned_invite_tokens_cleanup_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe OrphanedInviteTokensCleanup, :migration do
+RSpec.describe OrphanedInviteTokensCleanup, :migration, feature_category: :subgroups do
def create_member(**extra_attributes)
defaults = {
access_level: 10,
diff --git a/spec/migrations/orphaned_invited_members_cleanup_spec.rb b/spec/migrations/orphaned_invited_members_cleanup_spec.rb
index 4427e707f56..1d4db5306bc 100644
--- a/spec/migrations/orphaned_invited_members_cleanup_spec.rb
+++ b/spec/migrations/orphaned_invited_members_cleanup_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe OrphanedInvitedMembersCleanup, :migration do
+RSpec.describe OrphanedInvitedMembersCleanup, :migration, feature_category: :subgroups do
describe '#up', :aggregate_failures do
it 'removes accepted members with no associated user' do
user = create_user!('testuser1')
diff --git a/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb b/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb
index b3fe1776183..e2c117903d4 100644
--- a/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb
+++ b/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe PopulateAuditEventStreamingVerificationToken do
+RSpec.describe PopulateAuditEventStreamingVerificationToken, feature_category: :audit_events do
let(:groups) { table(:namespaces) }
let(:destinations) { table(:audit_events_external_audit_event_destinations) }
let(:migration) { described_class.new }
diff --git a/spec/migrations/populate_dismissal_information_for_vulnerabilities_spec.rb b/spec/migrations/populate_dismissal_information_for_vulnerabilities_spec.rb
index 1db52781956..66fd5eb5ae5 100644
--- a/spec/migrations/populate_dismissal_information_for_vulnerabilities_spec.rb
+++ b/spec/migrations/populate_dismissal_information_for_vulnerabilities_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe PopulateDismissalInformationForVulnerabilities do
+RSpec.describe PopulateDismissalInformationForVulnerabilities, feature_category: :vulnerability_management do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/populate_operation_visibility_permissions_spec.rb b/spec/migrations/populate_operation_visibility_permissions_spec.rb
index 6737a6f84c3..704152bd6a9 100644
--- a/spec/migrations/populate_operation_visibility_permissions_spec.rb
+++ b/spec/migrations/populate_operation_visibility_permissions_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe PopulateOperationVisibilityPermissions, :migration do
+RSpec.describe PopulateOperationVisibilityPermissions, :migration, feature_category: :navigation do
let(:migration) { described_class::MIGRATION }
before do
diff --git a/spec/migrations/populate_releases_access_level_from_repository_spec.rb b/spec/migrations/populate_releases_access_level_from_repository_spec.rb
index 2bb97662923..ebb7aa6f7fa 100644
--- a/spec/migrations/populate_releases_access_level_from_repository_spec.rb
+++ b/spec/migrations/populate_releases_access_level_from_repository_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe PopulateReleasesAccessLevelFromRepository, :migration do
+RSpec.describe PopulateReleasesAccessLevelFromRepository, :migration, feature_category: :navigation do
let(:projects) { table(:projects) }
let(:groups) { table(:namespaces) }
let(:project_features) { table(:project_features) }
diff --git a/spec/migrations/queue_backfill_project_feature_package_registry_access_level_spec.rb b/spec/migrations/queue_backfill_project_feature_package_registry_access_level_spec.rb
index 487d94b82a1..6a01b30445b 100644
--- a/spec/migrations/queue_backfill_project_feature_package_registry_access_level_spec.rb
+++ b/spec/migrations/queue_backfill_project_feature_package_registry_access_level_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe QueueBackfillProjectFeaturePackageRegistryAccessLevel do
- let_it_be(:batched_migration) { described_class::MIGRATION }
+RSpec.describe QueueBackfillProjectFeaturePackageRegistryAccessLevel, feature_category: :package_registry do
+ let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
diff --git a/spec/migrations/queue_backfill_user_details_fields_spec.rb b/spec/migrations/queue_backfill_user_details_fields_spec.rb
index 388ac6d1bce..e77a66907de 100644
--- a/spec/migrations/queue_backfill_user_details_fields_spec.rb
+++ b/spec/migrations/queue_backfill_user_details_fields_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe QueueBackfillUserDetailsFields do
- let_it_be(:batched_migration) { described_class::MIGRATION }
+RSpec.describe QueueBackfillUserDetailsFields, feature_category: :users do
+ let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
diff --git a/spec/migrations/queue_populate_projects_star_count_spec.rb b/spec/migrations/queue_populate_projects_star_count_spec.rb
index 848136d8005..84565d14d52 100644
--- a/spec/migrations/queue_populate_projects_star_count_spec.rb
+++ b/spec/migrations/queue_populate_projects_star_count_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe QueuePopulateProjectsStarCount do
- let_it_be(:batched_migration) { described_class::MIGRATION }
+RSpec.describe QueuePopulateProjectsStarCount, feature_category: :users do
+ let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
diff --git a/spec/migrations/re_schedule_latest_pipeline_id_population_with_all_security_related_artifact_types_spec.rb b/spec/migrations/re_schedule_latest_pipeline_id_population_with_all_security_related_artifact_types_spec.rb
index 45a2772adda..5ebe6787f15 100644
--- a/spec/migrations/re_schedule_latest_pipeline_id_population_with_all_security_related_artifact_types_spec.rb
+++ b/spec/migrations/re_schedule_latest_pipeline_id_population_with_all_security_related_artifact_types_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
RSpec.describe ReScheduleLatestPipelineIdPopulationWithAllSecurityRelatedArtifactTypes,
- :suppress_gitlab_schemas_validate_connection do
+ :suppress_gitlab_schemas_validate_connection, feature_category: :vulnerability_management do
let(:namespaces) { table(:namespaces) }
let(:pipelines) { table(:ci_pipelines) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/recount_epic_cache_counts_spec.rb b/spec/migrations/recount_epic_cache_counts_spec.rb
index 56aa96cb40c..d065389a726 100644
--- a/spec/migrations/recount_epic_cache_counts_spec.rb
+++ b/spec/migrations/recount_epic_cache_counts_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe RecountEpicCacheCounts, :migration do
+RSpec.describe RecountEpicCacheCounts, :migration, feature_category: :portfolio_management do
let(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_features_spec.rb b/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_features_spec.rb
index 77824a743fb..80ecc23dfbe 100644
--- a/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_features_spec.rb
+++ b/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_features_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe RecreateIndexSecurityCiBuildsOnNameAndIdParserFeatures, :migration do
+RSpec.describe RecreateIndexSecurityCiBuildsOnNameAndIdParserFeatures, :migration, feature_category: :database do
let(:db) { described_class.new }
let(:pg_class) { table(:pg_class) }
let(:pg_index) { table(:pg_index) }
diff --git a/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb b/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb
index 8ec51d86779..c7709764727 100644
--- a/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb
+++ b/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe RecreateIndexSecurityCiBuildsOnNameAndIdParserWithNewFeatures, :migration do
+RSpec.describe RecreateIndexSecurityCiBuildsOnNameAndIdParserWithNewFeatures, :migration, feature_category: :continuous_integration do
let(:db) { described_class.new }
let(:pg_class) { table(:pg_class) }
let(:pg_index) { table(:pg_index) }
diff --git a/spec/migrations/remove_duplicate_dast_site_tokens_spec.rb b/spec/migrations/remove_duplicate_dast_site_tokens_spec.rb
index fed9941b2a4..2b21dc3b67f 100644
--- a/spec/migrations/remove_duplicate_dast_site_tokens_spec.rb
+++ b/spec/migrations/remove_duplicate_dast_site_tokens_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe RemoveDuplicateDastSiteTokens do
+RSpec.describe RemoveDuplicateDastSiteTokens, feature_category: :dynamic_application_security_testing do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:dast_site_tokens) { table(:dast_site_tokens) }
@@ -15,7 +15,7 @@ RSpec.describe RemoveDuplicateDastSiteTokens do
context 'when duplicate dast site tokens exists' do
# create duplicate dast site token
- let_it_be(:duplicate_url) { 'https://about.gitlab.com' }
+ let!(:duplicate_url) { 'https://about.gitlab.com' }
let!(:project2) { projects.create!(id: 2, namespace_id: namespace.id, path: 'project2') }
let!(:dast_site_token2) { dast_site_tokens.create!(project_id: project2.id, url: duplicate_url, token: SecureRandom.uuid) }
diff --git a/spec/migrations/remove_duplicate_dast_site_tokens_with_same_token_spec.rb b/spec/migrations/remove_duplicate_dast_site_tokens_with_same_token_spec.rb
index 57d677af5cf..6cc25b74d02 100644
--- a/spec/migrations/remove_duplicate_dast_site_tokens_with_same_token_spec.rb
+++ b/spec/migrations/remove_duplicate_dast_site_tokens_with_same_token_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe RemoveDuplicateDastSiteTokensWithSameToken do
+RSpec.describe RemoveDuplicateDastSiteTokensWithSameToken, feature_category: :dynamic_application_security_testing do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:dast_site_tokens) { table(:dast_site_tokens) }
@@ -15,8 +15,8 @@ RSpec.describe RemoveDuplicateDastSiteTokensWithSameToken do
context 'when duplicate dast site tokens exists' do
# create duplicate dast site token
- let_it_be(:duplicate_token) { 'duplicate_token' }
- let_it_be(:other_duplicate_token) { 'other_duplicate_token' }
+ let!(:duplicate_token) { 'duplicate_token' }
+ let!(:other_duplicate_token) { 'other_duplicate_token' }
let!(:project2) { projects.create!(id: 2, namespace_id: namespace.id, path: 'project2') }
let!(:dast_site_token2) { dast_site_tokens.create!(project_id: project2.id, url: 'https://gitlab2.com', token: duplicate_token) }
diff --git a/spec/migrations/remove_flowdock_integration_records_spec.rb b/spec/migrations/remove_flowdock_integration_records_spec.rb
new file mode 100644
index 00000000000..3f57515d18b
--- /dev/null
+++ b/spec/migrations/remove_flowdock_integration_records_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db/post_migrate/20221129124240_remove_flowdock_integration_records.rb')
+
+RSpec.describe RemoveFlowdockIntegrationRecords, feature_category: :integrations do
+ let(:integrations) { table(:integrations) }
+
+ before do
+ integrations.create!(type_new: 'Integrations::Flowdock')
+ integrations.create!(type_new: 'SomeOtherType')
+ end
+
+ it 'removes integrations records of type_new Integrations::Flowdock' do
+ expect(integrations.count).to eq(2)
+
+ migrate!
+
+ expect(integrations.count).to eq(1)
+ expect(integrations.first.type_new).to eq('SomeOtherType')
+ expect(integrations.where(type_new: 'Integrations::Flowdock')).to be_empty
+ end
+end
diff --git a/spec/migrations/remove_hipchat_service_records_spec.rb b/spec/migrations/remove_hipchat_service_records_spec.rb
index d218b6b23a5..b89572b069e 100644
--- a/spec/migrations/remove_hipchat_service_records_spec.rb
+++ b/spec/migrations/remove_hipchat_service_records_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe RemoveHipchatServiceRecords do
+RSpec.describe RemoveHipchatServiceRecords, feature_category: :integrations do
let(:services) { table(:services) }
before do
diff --git a/spec/migrations/remove_invalid_integrations_spec.rb b/spec/migrations/remove_invalid_integrations_spec.rb
index cab2d79998e..52adc087e0a 100644
--- a/spec/migrations/remove_invalid_integrations_spec.rb
+++ b/spec/migrations/remove_invalid_integrations_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe RemoveInvalidIntegrations, :migration do
+RSpec.describe RemoveInvalidIntegrations, :migration, feature_category: :integrations do
describe '#up' do
let!(:integrations) { table(:integrations) }
diff --git a/spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb b/spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb
index 198644fe183..91687d8d730 100644
--- a/spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb
+++ b/spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe RemoveNotNullContraintOnTitleFromSprints, :migration do
+RSpec.describe RemoveNotNullContraintOnTitleFromSprints, :migration, feature_category: :team_planning do
let(:migration) { described_class.new }
let(:namespaces) { table(:namespaces) }
let(:sprints) { table(:sprints) }
diff --git a/spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb b/spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb
index c267e419b42..eabf6271ded 100644
--- a/spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb
+++ b/spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
require_migration!
require_migration!('add_not_valid_foreign_key_to_group_hooks')
-RSpec.describe RemoveRecordsWithoutGroupFromWebhooksTable, schema: 20210330091751 do
+RSpec.describe RemoveRecordsWithoutGroupFromWebhooksTable, schema: 20210330091751, feature_category: :integrations do
let(:web_hooks) { table(:web_hooks) }
let(:groups) { table(:namespaces) }
diff --git a/spec/migrations/remove_schedule_and_status_from_pending_alert_escalations_spec.rb b/spec/migrations/remove_schedule_and_status_from_pending_alert_escalations_spec.rb
index f595261ff90..86e161cea43 100644
--- a/spec/migrations/remove_schedule_and_status_from_pending_alert_escalations_spec.rb
+++ b/spec/migrations/remove_schedule_and_status_from_pending_alert_escalations_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe RemoveScheduleAndStatusFromPendingAlertEscalations do
+RSpec.describe RemoveScheduleAndStatusFromPendingAlertEscalations, feature_category: :incident_management do
let(:escalations) { table(:incident_management_pending_alert_escalations) }
let(:schedule_index) { 'index_incident_management_pending_alert_escalations_on_schedule' }
let(:schedule_foreign_key) { 'fk_rails_fcbfd9338b' }
diff --git a/spec/migrations/remove_wiki_notes_spec.rb b/spec/migrations/remove_wiki_notes_spec.rb
index 2ffebdee106..55f58ef7be6 100644
--- a/spec/migrations/remove_wiki_notes_spec.rb
+++ b/spec/migrations/remove_wiki_notes_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe RemoveWikiNotes, :migration do
+RSpec.describe RemoveWikiNotes, :migration, feature_category: :team_planning do
let(:notes) { table(:notes) }
it 'removes all wiki notes' do
diff --git a/spec/migrations/rename_services_to_integrations_spec.rb b/spec/migrations/rename_services_to_integrations_spec.rb
index 812dd5efecb..a90b0bfabd2 100644
--- a/spec/migrations/rename_services_to_integrations_spec.rb
+++ b/spec/migrations/rename_services_to_integrations_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe RenameServicesToIntegrations do
+RSpec.describe RenameServicesToIntegrations, feature_category: :integrations do
let(:migration) { described_class.new }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/replace_external_wiki_triggers_spec.rb b/spec/migrations/replace_external_wiki_triggers_spec.rb
index 392ef76c5ba..c2bc5c44c77 100644
--- a/spec/migrations/replace_external_wiki_triggers_spec.rb
+++ b/spec/migrations/replace_external_wiki_triggers_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe ReplaceExternalWikiTriggers do
+RSpec.describe ReplaceExternalWikiTriggers, feature_category: :integrations do
let(:migration) { described_class.new }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/reschedule_backfill_imported_issue_search_data_spec.rb b/spec/migrations/reschedule_backfill_imported_issue_search_data_spec.rb
index 7581c201a59..fe730f452f7 100644
--- a/spec/migrations/reschedule_backfill_imported_issue_search_data_spec.rb
+++ b/spec/migrations/reschedule_backfill_imported_issue_search_data_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe RescheduleBackfillImportedIssueSearchData do
- let_it_be(:reschedule_migration) { described_class::MIGRATION }
+RSpec.describe RescheduleBackfillImportedIssueSearchData, feature_category: :global_search do
+ let!(:reschedule_migration) { described_class::MIGRATION }
def create_batched_migration(max_value:)
Gitlab::Database::BackgroundMigration::BatchedMigration
@@ -55,12 +55,23 @@ RSpec.describe RescheduleBackfillImportedIssueSearchData do
end
context 'when an issue is available' do
- let_it_be(:namespaces_table) { table(:namespaces) }
- let_it_be(:projects_table) { table(:projects) }
+ let!(:namespaces_table) { table(:namespaces) }
+ let!(:projects_table) { table(:projects) }
let(:namespace) { namespaces_table.create!(name: 'gitlab-org', path: 'gitlab-org') }
- let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: namespace.id, project_namespace_id: namespace.id) } # rubocop:disable Layout/LineLength
- let(:issue) { table(:issues).create!(project_id: project.id, title: 'test title', description: 'test description') }
+
+ let(:project) do
+ projects_table.create!(
+ name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: namespace.id, project_namespace_id: namespace.id
+ )
+ end
+
+ let(:issue) do
+ table(:issues).create!(
+ project_id: project.id, namespace_id: project.project_namespace_id,
+ title: 'test title', description: 'test description'
+ )
+ end
before do
create_batched_migration(max_value: max_value)
diff --git a/spec/migrations/reschedule_delete_orphaned_deployments_spec.rb b/spec/migrations/reschedule_delete_orphaned_deployments_spec.rb
index eb91602388c..bbc4494837a 100644
--- a/spec/migrations/reschedule_delete_orphaned_deployments_spec.rb
+++ b/spec/migrations/reschedule_delete_orphaned_deployments_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
require_migration!
-RSpec.describe RescheduleDeleteOrphanedDeployments, :sidekiq, schema: 20210617161348 do
+RSpec.describe RescheduleDeleteOrphanedDeployments, :sidekiq, schema: 20210617161348,
+ feature_category: :continuous_delivery do
let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
let!(:environment) { table(:environments).create!(name: 'production', slug: 'production', project_id: project.id) }
diff --git a/spec/migrations/reschedule_issue_work_item_type_id_backfill_spec.rb b/spec/migrations/reschedule_issue_work_item_type_id_backfill_spec.rb
index 126d49790a5..1443ff09241 100644
--- a/spec/migrations/reschedule_issue_work_item_type_id_backfill_spec.rb
+++ b/spec/migrations/reschedule_issue_work_item_type_id_backfill_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
require_migration!
-RSpec.describe RescheduleIssueWorkItemTypeIdBackfill, :migration do
- let_it_be(:migration) { described_class::MIGRATION }
- let_it_be(:interval) { 2.minutes }
- let_it_be(:issue_type_enum) { { issue: 0, incident: 1, test_case: 2, requirement: 3, task: 4 } }
- let_it_be(:base_work_item_type_ids) do
+RSpec.describe RescheduleIssueWorkItemTypeIdBackfill, :migration, feature_category: :team_planning do
+ let!(:migration) { described_class::MIGRATION }
+ let!(:interval) { 2.minutes }
+ let!(:issue_type_enum) { { issue: 0, incident: 1, test_case: 2, requirement: 3, task: 4 } }
+ let!(:base_work_item_type_ids) do
table(:work_item_types).where(namespace_id: nil).order(:base_type).each_with_object({}) do |type, hash|
hash[type.base_type] = type.id
end
diff --git a/spec/migrations/reschedule_migrate_shared_vulnerability_scanners_spec.rb b/spec/migrations/reschedule_migrate_shared_vulnerability_scanners_spec.rb
index e8253f39c68..48422de81fe 100644
--- a/spec/migrations/reschedule_migrate_shared_vulnerability_scanners_spec.rb
+++ b/spec/migrations/reschedule_migrate_shared_vulnerability_scanners_spec.rb
@@ -4,7 +4,7 @@ require "spec_helper"
require_migration!
-RSpec.describe RescheduleMigrateSharedVulnerabilityScanners, :migration do
+RSpec.describe RescheduleMigrateSharedVulnerabilityScanners, :migration, feature_category: :vulnerability_management do
include Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers
def connection
diff --git a/spec/migrations/reset_job_token_scope_enabled_again_spec.rb b/spec/migrations/reset_job_token_scope_enabled_again_spec.rb
index 8f9e12852e1..9f1180b6e24 100644
--- a/spec/migrations/reset_job_token_scope_enabled_again_spec.rb
+++ b/spec/migrations/reset_job_token_scope_enabled_again_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe ResetJobTokenScopeEnabledAgain do
+RSpec.describe ResetJobTokenScopeEnabledAgain, feature_category: :continuous_integration do
let(:settings) { table(:project_ci_cd_settings) }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/reset_job_token_scope_enabled_spec.rb b/spec/migrations/reset_job_token_scope_enabled_spec.rb
index fb7bd78c11f..4ce9078246a 100644
--- a/spec/migrations/reset_job_token_scope_enabled_spec.rb
+++ b/spec/migrations/reset_job_token_scope_enabled_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe ResetJobTokenScopeEnabled do
+RSpec.describe ResetJobTokenScopeEnabled, feature_category: :continuous_integration do
let(:settings) { table(:project_ci_cd_settings) }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/reset_severity_levels_to_new_default_spec.rb b/spec/migrations/reset_severity_levels_to_new_default_spec.rb
index c352f1f3cee..83e57b852a0 100644
--- a/spec/migrations/reset_severity_levels_to_new_default_spec.rb
+++ b/spec/migrations/reset_severity_levels_to_new_default_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe ResetSeverityLevelsToNewDefault do
+RSpec.describe ResetSeverityLevelsToNewDefault, feature_category: :source_code_management do
let(:approval_project_rules) { table(:approval_project_rules) }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/retry_backfill_traversal_ids_spec.rb b/spec/migrations/retry_backfill_traversal_ids_spec.rb
index 910be9f2c69..f3658d1b8a3 100644
--- a/spec/migrations/retry_backfill_traversal_ids_spec.rb
+++ b/spec/migrations/retry_backfill_traversal_ids_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require_migration!
-RSpec.describe RetryBackfillTraversalIds, :migration do
+RSpec.describe RetryBackfillTraversalIds, :migration, feature_category: :subgroups do
include ReloadHelpers
- let_it_be(:namespaces_table) { table(:namespaces) }
+ let!(:namespaces_table) { table(:namespaces) }
context 'when BackfillNamespaceTraversalIdsRoots jobs are pending' do
before do
diff --git a/spec/migrations/sanitize_confidential_note_todos_spec.rb b/spec/migrations/sanitize_confidential_note_todos_spec.rb
index 00dece82cc1..142378e07e1 100644
--- a/spec/migrations/sanitize_confidential_note_todos_spec.rb
+++ b/spec/migrations/sanitize_confidential_note_todos_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe SanitizeConfidentialNoteTodos do
+RSpec.describe SanitizeConfidentialNoteTodos, feature_category: :team_planning do
let(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/schedule_add_primary_email_to_emails_if_user_confirmed_spec.rb b/spec/migrations/schedule_add_primary_email_to_emails_if_user_confirmed_spec.rb
index c66ac1bd7e9..98d3e9b7c7c 100644
--- a/spec/migrations/schedule_add_primary_email_to_emails_if_user_confirmed_spec.rb
+++ b/spec/migrations/schedule_add_primary_email_to_emails_if_user_confirmed_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleAddPrimaryEmailToEmailsIfUserConfirmed, :sidekiq do
+RSpec.describe ScheduleAddPrimaryEmailToEmailsIfUserConfirmed, :sidekiq, feature_category: :users do
let(:migration) { described_class.new }
let(:users) { table(:users) }
diff --git a/spec/migrations/schedule_backfill_cluster_agents_has_vulnerabilities_spec.rb b/spec/migrations/schedule_backfill_cluster_agents_has_vulnerabilities_spec.rb
index 675cc332e69..84764c89adb 100644
--- a/spec/migrations/schedule_backfill_cluster_agents_has_vulnerabilities_spec.rb
+++ b/spec/migrations/schedule_backfill_cluster_agents_has_vulnerabilities_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleBackfillClusterAgentsHasVulnerabilities do
- let_it_be(:batched_migration) { described_class::MIGRATION }
+RSpec.describe ScheduleBackfillClusterAgentsHasVulnerabilities, feature_category: :vulnerability_management do
+ let!(:batched_migration) { described_class::MIGRATION }
it 'schedules background jobs for each batch of cluster agents' do
reversible_migration do |migration|
diff --git a/spec/migrations/schedule_backfill_draft_status_on_merge_requests_corrected_regex_spec.rb b/spec/migrations/schedule_backfill_draft_status_on_merge_requests_corrected_regex_spec.rb
index 9d7651d01ed..8a14bf58698 100644
--- a/spec/migrations/schedule_backfill_draft_status_on_merge_requests_corrected_regex_spec.rb
+++ b/spec/migrations/schedule_backfill_draft_status_on_merge_requests_corrected_regex_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe ScheduleBackfillDraftStatusOnMergeRequestsCorrectedRegex, :sidekiq do
+RSpec.describe ScheduleBackfillDraftStatusOnMergeRequestsCorrectedRegex, :sidekiq, feature_category: :code_review do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:merge_requests) { table(:merge_requests) }
diff --git a/spec/migrations/schedule_backfilling_the_namespace_id_for_vulnerability_reads_spec.rb b/spec/migrations/schedule_backfilling_the_namespace_id_for_vulnerability_reads_spec.rb
index e03096de98d..e547b321c52 100644
--- a/spec/migrations/schedule_backfilling_the_namespace_id_for_vulnerability_reads_spec.rb
+++ b/spec/migrations/schedule_backfilling_the_namespace_id_for_vulnerability_reads_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
require_migration!
-RSpec.describe ScheduleBackfillingTheNamespaceIdForVulnerabilityReads do
- let_it_be(:migration) { described_class::MIGRATION_NAME }
+RSpec.describe ScheduleBackfillingTheNamespaceIdForVulnerabilityReads, feature_category: :vulnerability_management do
+ let!(:migration) { described_class::MIGRATION_NAME }
describe '#up' do
it 'schedules background jobs for each batch of vulnerabilities' do
diff --git a/spec/migrations/schedule_copy_ci_builds_columns_to_security_scans2_spec.rb b/spec/migrations/schedule_copy_ci_builds_columns_to_security_scans2_spec.rb
index 67d54ea92a0..63678a094a7 100644
--- a/spec/migrations/schedule_copy_ci_builds_columns_to_security_scans2_spec.rb
+++ b/spec/migrations/schedule_copy_ci_builds_columns_to_security_scans2_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleCopyCiBuildsColumnsToSecurityScans2 do
+RSpec.describe ScheduleCopyCiBuildsColumnsToSecurityScans2, feature_category: :dependency_scanning do
it 'is a no-op' do
migrate!
end
diff --git a/spec/migrations/schedule_disable_expiration_policies_linked_to_no_container_images_spec.rb b/spec/migrations/schedule_disable_expiration_policies_linked_to_no_container_images_spec.rb
index 888d306f893..ebcc3fda0a3 100644
--- a/spec/migrations/schedule_disable_expiration_policies_linked_to_no_container_images_spec.rb
+++ b/spec/migrations/schedule_disable_expiration_policies_linked_to_no_container_images_spec.rb
@@ -4,22 +4,22 @@ require 'spec_helper'
require_migration!
-RSpec.describe ScheduleDisableExpirationPoliciesLinkedToNoContainerImages do
- let_it_be(:projects) { table(:projects) }
- let_it_be(:container_expiration_policies) { table(:container_expiration_policies) }
- let_it_be(:container_repositories) { table(:container_repositories) }
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:namespace) { namespaces.create!(name: 'test', path: 'test') }
-
- let_it_be(:policy1) { create_expiration_policy(id: 1, enabled: true) }
- let_it_be(:policy2) { create_expiration_policy(id: 2, enabled: false) }
- let_it_be(:policy3) { create_expiration_policy(id: 3, enabled: false) }
- let_it_be(:policy4) { create_expiration_policy(id: 4, enabled: true) }
- let_it_be(:policy5) { create_expiration_policy(id: 5, enabled: false) }
- let_it_be(:policy6) { create_expiration_policy(id: 6, enabled: false) }
- let_it_be(:policy7) { create_expiration_policy(id: 7, enabled: true) }
- let_it_be(:policy8) { create_expiration_policy(id: 8, enabled: true) }
- let_it_be(:policy9) { create_expiration_policy(id: 9, enabled: true) }
+RSpec.describe ScheduleDisableExpirationPoliciesLinkedToNoContainerImages, feature_category: :container_registry do
+ let!(:projects) { table(:projects) }
+ let!(:container_expiration_policies) { table(:container_expiration_policies) }
+ let!(:container_repositories) { table(:container_repositories) }
+ let!(:namespaces) { table(:namespaces) }
+ let!(:namespace) { namespaces.create!(name: 'test', path: 'test') }
+
+ let!(:policy1) { create_expiration_policy(id: 1, enabled: true) }
+ let!(:policy2) { create_expiration_policy(id: 2, enabled: false) }
+ let!(:policy3) { create_expiration_policy(id: 3, enabled: false) }
+ let!(:policy4) { create_expiration_policy(id: 4, enabled: true) }
+ let!(:policy5) { create_expiration_policy(id: 5, enabled: false) }
+ let!(:policy6) { create_expiration_policy(id: 6, enabled: false) }
+ let!(:policy7) { create_expiration_policy(id: 7, enabled: true) }
+ let!(:policy8) { create_expiration_policy(id: 8, enabled: true) }
+ let!(:policy9) { create_expiration_policy(id: 9, enabled: true) }
it 'schedules background migrations', :aggregate_failures do
stub_const("#{described_class}::BATCH_SIZE", 2)
diff --git a/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb b/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb
index 3720be6cf3e..26764f855b7 100644
--- a/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb
+++ b/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleFixIncorrectMaxSeatsUsed2, :migration do
+RSpec.describe ScheduleFixIncorrectMaxSeatsUsed2, :migration, feature_category: :purchase do
let(:migration_name) { described_class::MIGRATION.to_s.demodulize }
describe '#up' do
diff --git a/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb b/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb
index 74258f03630..194a1d39ad1 100644
--- a/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb
+++ b/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleFixIncorrectMaxSeatsUsed, :migration do
+RSpec.describe ScheduleFixIncorrectMaxSeatsUsed, :migration, feature_category: :purchase do
let(:migration) { described_class.new }
describe '#up' do
diff --git a/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb b/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb
new file mode 100644
index 00000000000..c4c7819bda7
--- /dev/null
+++ b/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleFixingSecurityScanStatuses, :suppress_gitlab_schemas_validate_connection,
+ feature_category: :vulnerability_management do
+ let!(:namespaces) { table(:namespaces) }
+ let!(:projects) { table(:projects) }
+ let!(:pipelines) { table(:ci_pipelines) }
+ let!(:builds) { table(:ci_builds) }
+ 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)
+ end
+
+ let!(:ci_build) { builds.create!(commit_id: pipeline.id, retried: false, type: 'Ci::Build', partition_id: 1) }
+
+ 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) }
+
+ let(:com?) { false }
+ let(:dev_or_test_env?) { false }
+ let(:migration) { described_class::MIGRATION }
+
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(com?)
+ allow(::Gitlab).to receive(:dev_or_test_env?).and_return(dev_or_test_env?)
+
+ migrate!
+ end
+
+ describe '#up' do
+ shared_examples_for 'scheduler for fixing the security scans status' do
+ it 'schedules background job' do
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :security_scans,
+ column_name: :id,
+ interval: 2.minutes,
+ batch_size: 10_000,
+ max_batch_size: 50_000,
+ sub_batch_size: 100,
+ batch_min_value: security_scan_2.id
+ )
+ end
+ end
+
+ context 'when the migration does not run on GitLab.com or development environment' do
+ it 'does not schedule the migration' do
+ expect('FixSecurityScanStatuses').not_to have_scheduled_batched_migration
+ end
+ end
+
+ context 'when the migration runs on GitLab.com' do
+ let(:com?) { true }
+
+ it_behaves_like 'scheduler for fixing the security scans status'
+ end
+
+ context 'when the migration runs on dev environment' do
+ let(:dev_or_test_env?) { true }
+
+ it_behaves_like 'scheduler for fixing the security scans status'
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/schedule_populate_requirements_issue_id_spec.rb b/spec/migrations/schedule_populate_requirements_issue_id_spec.rb
index 2702c000b60..000c42cc4fc 100644
--- a/spec/migrations/schedule_populate_requirements_issue_id_spec.rb
+++ b/spec/migrations/schedule_populate_requirements_issue_id_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe SchedulePopulateRequirementsIssueId do
+RSpec.describe SchedulePopulateRequirementsIssueId, feature_category: :requirements_management do
include MigrationHelpers::WorkItemTypesHelper
let(:issues) { table(:issues) }
@@ -48,7 +48,7 @@ RSpec.describe SchedulePopulateRequirementsIssueId do
# Create one requirement with issue_id present, to make
# sure a job won't be scheduled for it
- work_item_type_id = work_item_types_table.find_by(namespace_id: nil, name: 'Issue').id
+ work_item_type_id = table(:work_item_types).find_by(namespace_id: nil, name: 'Issue').id
issue = issues.create!(state_id: 1, work_item_type_id: work_item_type_id)
create_requirement(iid: 2, title: 'r 2', issue_id: issue.id)
diff --git a/spec/migrations/schedule_purging_stale_security_scans_spec.rb b/spec/migrations/schedule_purging_stale_security_scans_spec.rb
index b5a38634b58..b39baa145ff 100644
--- a/spec/migrations/schedule_purging_stale_security_scans_spec.rb
+++ b/spec/migrations/schedule_purging_stale_security_scans_spec.rb
@@ -3,17 +3,18 @@
require 'spec_helper'
require_migration!
-RSpec.describe SchedulePurgingStaleSecurityScans do
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:projects) { table(:projects) }
- let_it_be(:pipelines) { table(:ci_pipelines) }
- let_it_be(:builds) { table(:ci_builds) }
- let_it_be(:security_scans) { table(:security_scans) }
-
- let_it_be(:namespace) { namespaces.create!(name: "foo", path: "bar") }
- let_it_be(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) }
- let_it_be(:pipeline) { pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a', status: 'success') }
- let_it_be(:ci_build) { builds.create!(commit_id: pipeline.id, retried: false, type: 'Ci::Build') }
+RSpec.describe SchedulePurgingStaleSecurityScans, :suppress_gitlab_schemas_validate_connection,
+feature_category: :vulnerability_management do
+ let!(:namespaces) { table(:namespaces) }
+ let!(:projects) { table(:projects) }
+ let!(:pipelines) { table(:ci_pipelines) }
+ let!(:builds) { table(:ci_builds) }
+ 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) { pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a', status: 'success') }
+ let!(:ci_build) { builds.create!(commit_id: pipeline.id, retried: false, type: 'Ci::Build') }
let!(:security_scan_1) { security_scans.create!(build_id: ci_build.id, scan_type: 1, created_at: 92.days.ago) }
let!(:security_scan_2) { security_scans.create!(build_id: ci_build.id, scan_type: 2, created_at: 91.days.ago) }
diff --git a/spec/migrations/schedule_recalculate_vulnerability_finding_signatures_for_findings_spec.rb b/spec/migrations/schedule_recalculate_vulnerability_finding_signatures_for_findings_spec.rb
index 9b62dd79e08..8903a32285e 100644
--- a/spec/migrations/schedule_recalculate_vulnerability_finding_signatures_for_findings_spec.rb
+++ b/spec/migrations/schedule_recalculate_vulnerability_finding_signatures_for_findings_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleRecalculateVulnerabilityFindingSignaturesForFindings, :migration do
+RSpec.describe ScheduleRecalculateVulnerabilityFindingSignaturesForFindings, :migration,
+feature_category: :vulnerability_management do
before do
allow(Gitlab).to receive(:ee?).and_return(ee?)
stub_const("#{described_class.name}::BATCH_SIZE", 2)
@@ -20,21 +21,21 @@ RSpec.describe ScheduleRecalculateVulnerabilityFindingSignaturesForFindings, :mi
context 'when the Gitlab instance is EE' do
let(:ee?) { true }
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:projects) { table(:projects) }
- let_it_be(:findings) { table(:vulnerability_occurrences) }
- let_it_be(:scanners) { table(:vulnerability_scanners) }
- let_it_be(:identifiers) { table(:vulnerability_identifiers) }
- let_it_be(:vulnerability_finding_signatures) { table(:vulnerability_finding_signatures) }
+ let!(:namespaces) { table(:namespaces) }
+ let!(:projects) { table(:projects) }
+ let!(:findings) { table(:vulnerability_occurrences) }
+ let!(:scanners) { table(:vulnerability_scanners) }
+ let!(:identifiers) { table(:vulnerability_identifiers) }
+ let!(:vulnerability_finding_signatures) { table(:vulnerability_finding_signatures) }
- let_it_be(:namespace) { namespaces.create!(name: 'test', path: 'test') }
- let_it_be(:project) { projects.create!(namespace_id: namespace.id, name: 'gitlab', path: 'gitlab') }
+ let!(:namespace) { namespaces.create!(name: 'test', path: 'test') }
+ let!(:project) { projects.create!(namespace_id: namespace.id, name: 'gitlab', path: 'gitlab') }
- let_it_be(:scanner) do
+ let!(:scanner) do
scanners.create!(project_id: project.id, external_id: 'trivy', name: 'Security Scanner')
end
- let_it_be(:identifier) do
+ let!(:identifier) do
identifiers.create!(project_id: project.id,
fingerprint: 'd432c2ad2953e8bd587a3a43b3ce309b5b0154c123',
external_type: 'SECURITY_ID',
@@ -42,14 +43,14 @@ RSpec.describe ScheduleRecalculateVulnerabilityFindingSignaturesForFindings, :mi
name: 'SECURITY_IDENTIFIER 0')
end
- let_it_be(:finding1) { findings.create!(finding_params) }
- let_it_be(:signature1) { vulnerability_finding_signatures.create!(finding_id: finding1.id, algorithm_type: 0, signature_sha: ::Digest::SHA1.digest(SecureRandom.hex(50))) }
+ let!(:finding1) { findings.create!(finding_params) }
+ let!(:signature1) { vulnerability_finding_signatures.create!(finding_id: finding1.id, algorithm_type: 0, signature_sha: ::Digest::SHA1.digest(SecureRandom.hex(50))) }
- let_it_be(:finding2) { findings.create!(finding_params) }
- let_it_be(:signature2) { vulnerability_finding_signatures.create!(finding_id: finding2.id, algorithm_type: 0, signature_sha: ::Digest::SHA1.digest(SecureRandom.hex(50))) }
+ let!(:finding2) { findings.create!(finding_params) }
+ let!(:signature2) { vulnerability_finding_signatures.create!(finding_id: finding2.id, algorithm_type: 0, signature_sha: ::Digest::SHA1.digest(SecureRandom.hex(50))) }
- let_it_be(:finding3) { findings.create!(finding_params) }
- let_it_be(:signature3) { vulnerability_finding_signatures.create!(finding_id: finding3.id, algorithm_type: 0, signature_sha: ::Digest::SHA1.digest(SecureRandom.hex(50))) }
+ let!(:finding3) { findings.create!(finding_params) }
+ let!(:signature3) { vulnerability_finding_signatures.create!(finding_id: finding3.id, algorithm_type: 0, signature_sha: ::Digest::SHA1.digest(SecureRandom.hex(50))) }
# this migration is now a no-op
it 'does not schedule the background jobs', :aggregate_failure do
diff --git a/spec/migrations/schedule_security_setting_creation_spec.rb b/spec/migrations/schedule_security_setting_creation_spec.rb
index e1b7b540d7f..edabb2a2299 100644
--- a/spec/migrations/schedule_security_setting_creation_spec.rb
+++ b/spec/migrations/schedule_security_setting_creation_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleSecuritySettingCreation, :sidekiq do
+RSpec.describe ScheduleSecuritySettingCreation, :sidekiq, feature_category: :projects do
describe '#up' do
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/schedule_set_correct_vulnerability_state_spec.rb b/spec/migrations/schedule_set_correct_vulnerability_state_spec.rb
index 08dccf1f37a..e888a1132c0 100644
--- a/spec/migrations/schedule_set_correct_vulnerability_state_spec.rb
+++ b/spec/migrations/schedule_set_correct_vulnerability_state_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
require_migration!
-RSpec.describe ScheduleSetCorrectVulnerabilityState do
- let_it_be(:migration) { described_class::MIGRATION_NAME }
+RSpec.describe ScheduleSetCorrectVulnerabilityState, feature_category: :vulnerability_management do
+ let!(:migration) { described_class::MIGRATION_NAME }
describe '#up' do
it 'schedules background jobs for each batch of vulnerabilities' do
diff --git a/spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb b/spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb
index a81059518e6..99ee9e58f4e 100644
--- a/spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb
+++ b/spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb
@@ -3,18 +3,18 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleUpdateTimelogsNullSpentAt do
- let_it_be(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
- let_it_be(:project) { table(:projects).create!(namespace_id: namespace.id) }
- let_it_be(:issue) { table(:issues).create!(project_id: project.id) }
- let_it_be(:merge_request) { table(:merge_requests).create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature') }
- let_it_be(:timelog1) { create_timelog!(merge_request_id: merge_request.id) }
- let_it_be(:timelog2) { create_timelog!(merge_request_id: merge_request.id) }
- let_it_be(:timelog3) { create_timelog!(merge_request_id: merge_request.id) }
- let_it_be(:timelog4) { create_timelog!(issue_id: issue.id) }
- let_it_be(:timelog5) { create_timelog!(issue_id: issue.id) }
-
- before_all do
+RSpec.describe ScheduleUpdateTimelogsNullSpentAt, feature_category: :team_planning do
+ let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
+ let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:issue) { table(:issues).create!(project_id: project.id) }
+ let!(:merge_request) { table(:merge_requests).create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature') }
+ let!(:timelog1) { create_timelog!(merge_request_id: merge_request.id) }
+ let!(:timelog2) { create_timelog!(merge_request_id: merge_request.id) }
+ let!(:timelog3) { create_timelog!(merge_request_id: merge_request.id) }
+ let!(:timelog4) { create_timelog!(issue_id: issue.id) }
+ let!(:timelog5) { create_timelog!(issue_id: issue.id) }
+
+ before do
table(:timelogs).where.not(id: timelog3.id).update_all(spent_at: nil)
end
diff --git a/spec/migrations/schedule_update_timelogs_project_id_spec.rb b/spec/migrations/schedule_update_timelogs_project_id_spec.rb
index b9130fd86be..5ce3f7dd36c 100644
--- a/spec/migrations/schedule_update_timelogs_project_id_spec.rb
+++ b/spec/migrations/schedule_update_timelogs_project_id_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleUpdateTimelogsProjectId do
+RSpec.describe ScheduleUpdateTimelogsProjectId, feature_category: :team_planning do
let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
let!(:issue) { table(:issues).create!(project_id: project.id) }
diff --git a/spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb b/spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb
index 2fe739659f0..c9f22c02a0b 100644
--- a/spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb
+++ b/spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleUpdateUsersWhereTwoFactorAuthRequiredFromGroup do
+RSpec.describe ScheduleUpdateUsersWhereTwoFactorAuthRequiredFromGroup, feature_category: :require_two_factor_authentication_from_group do
let(:users) { table(:users) }
let!(:user_1) { users.create!(require_two_factor_authentication_from_group: false, name: "user1", email: "user1@example.com", projects_limit: 1) }
let!(:user_2) { users.create!(require_two_factor_authentication_from_group: true, name: "user2", email: "user2@example.com", projects_limit: 1) }
diff --git a/spec/migrations/set_default_job_token_scope_true_spec.rb b/spec/migrations/set_default_job_token_scope_true_spec.rb
index e7c77357318..25f4f07e15a 100644
--- a/spec/migrations/set_default_job_token_scope_true_spec.rb
+++ b/spec/migrations/set_default_job_token_scope_true_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe SetDefaultJobTokenScopeTrue, schema: 20210819153805 do
+RSpec.describe SetDefaultJobTokenScopeTrue, schema: 20210819153805, feature_category: :continuous_integration do
let(:ci_cd_settings) { table(:project_ci_cd_settings) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/set_email_confirmation_setting_before_removing_send_user_confirmation_email_column_spec.rb b/spec/migrations/set_email_confirmation_setting_before_removing_send_user_confirmation_email_column_spec.rb
new file mode 100644
index 00000000000..4303713744e
--- /dev/null
+++ b/spec/migrations/set_email_confirmation_setting_before_removing_send_user_confirmation_email_column_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SetEmailConfirmationSettingBeforeRemovingSendUserConfirmationEmailColumn, feature_category: :users do
+ let(:migration) { described_class.new }
+ let(:application_settings_table) { table(:application_settings) }
+
+ describe '#up' do
+ context "when 'send_user_confirmation_email' is set to 'true'" do
+ it "updates 'email_confirmation_setting' to '2' (hard)" do
+ application_settings_table.create!(send_user_confirmation_email: true, email_confirmation_setting: 0)
+
+ migration.up
+
+ expect(application_settings_table.last.email_confirmation_setting).to eq 2
+ end
+ end
+
+ context "when 'send_user_confirmation_email' is set to 'false'" do
+ it "updates 'email_confirmation_setting' to '0' (off)" do
+ application_settings_table.create!(send_user_confirmation_email: false, email_confirmation_setting: 0)
+
+ migration.up
+
+ expect(application_settings_table.last.email_confirmation_setting).to eq 0
+ end
+ end
+ end
+
+ describe '#down' do
+ it "updates 'email_confirmation_setting' to default value: '0' (off)" do
+ application_settings_table.create!(send_user_confirmation_email: true, email_confirmation_setting: 2)
+
+ migration.down
+
+ expect(application_settings_table.last.email_confirmation_setting).to eq 0
+ end
+ end
+end
diff --git a/spec/migrations/set_email_confirmation_setting_from_send_user_confirmation_email_setting_spec.rb b/spec/migrations/set_email_confirmation_setting_from_send_user_confirmation_email_setting_spec.rb
index 761c0ef2fdb..e08aa8679a1 100644
--- a/spec/migrations/set_email_confirmation_setting_from_send_user_confirmation_email_setting_spec.rb
+++ b/spec/migrations/set_email_confirmation_setting_from_send_user_confirmation_email_setting_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe SetEmailConfirmationSettingFromSendUserConfirmationEmailSetting do
+RSpec.describe SetEmailConfirmationSettingFromSendUserConfirmationEmailSetting, feature_category: :users do
let(:migration) { described_class.new }
let(:application_settings_table) { table(:application_settings) }
diff --git a/spec/migrations/slice_merge_request_diff_commit_migrations_spec.rb b/spec/migrations/slice_merge_request_diff_commit_migrations_spec.rb
index b03a5c41a11..fdbd8093fa5 100644
--- a/spec/migrations/slice_merge_request_diff_commit_migrations_spec.rb
+++ b/spec/migrations/slice_merge_request_diff_commit_migrations_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe SliceMergeRequestDiffCommitMigrations, :migration do
+RSpec.describe SliceMergeRequestDiffCommitMigrations, :migration, feature_category: :code_review do
let(:migration) { described_class.new }
describe '#up' do
diff --git a/spec/migrations/start_backfill_ci_queuing_tables_spec.rb b/spec/migrations/start_backfill_ci_queuing_tables_spec.rb
index 08fd244089f..c308a16d5b8 100644
--- a/spec/migrations/start_backfill_ci_queuing_tables_spec.rb
+++ b/spec/migrations/start_backfill_ci_queuing_tables_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe StartBackfillCiQueuingTables, :suppress_gitlab_schemas_validate_connection do
+RSpec.describe StartBackfillCiQueuingTables, :suppress_gitlab_schemas_validate_connection,
+feature_category: :continuous_integration do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:builds) { table(:ci_builds) }
diff --git a/spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb b/spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb
index 4fb4ba61a34..d2cd7a6980d 100644
--- a/spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb
+++ b/spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe StealMergeRequestDiffCommitUsersMigration, :migration do
+RSpec.describe StealMergeRequestDiffCommitUsersMigration, :migration, feature_category: :source_code_management do
let(:migration) { described_class.new }
describe '#up' do
diff --git a/spec/migrations/sync_new_amount_used_for_ci_namespace_monthly_usages_spec.rb b/spec/migrations/sync_new_amount_used_for_ci_namespace_monthly_usages_spec.rb
index 9a17f375f82..da8790f4450 100644
--- a/spec/migrations/sync_new_amount_used_for_ci_namespace_monthly_usages_spec.rb
+++ b/spec/migrations/sync_new_amount_used_for_ci_namespace_monthly_usages_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
require_migration!
-RSpec.describe SyncNewAmountUsedForCiNamespaceMonthlyUsages, migration: :gitlab_ci do
+RSpec.describe SyncNewAmountUsedForCiNamespaceMonthlyUsages, migration: :gitlab_ci,
+ feature_category: :continuous_integration do
let(:namespace_usages) { table(:ci_namespace_monthly_usages) }
before do
diff --git a/spec/migrations/sync_new_amount_used_for_ci_project_monthly_usages_spec.rb b/spec/migrations/sync_new_amount_used_for_ci_project_monthly_usages_spec.rb
index 8d45f1107ea..1c9b2711687 100644
--- a/spec/migrations/sync_new_amount_used_for_ci_project_monthly_usages_spec.rb
+++ b/spec/migrations/sync_new_amount_used_for_ci_project_monthly_usages_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
require_migration!
-RSpec.describe SyncNewAmountUsedForCiProjectMonthlyUsages, migration: :gitlab_ci do
+RSpec.describe SyncNewAmountUsedForCiProjectMonthlyUsages, migration: :gitlab_ci,
+ feature_category: :continuous_integration do
let(:project_usages) { table(:ci_project_monthly_usages) }
before do
diff --git a/spec/migrations/toggle_vsa_aggregations_enable_spec.rb b/spec/migrations/toggle_vsa_aggregations_enable_spec.rb
index a6850d493b7..5b3e513e9f6 100644
--- a/spec/migrations/toggle_vsa_aggregations_enable_spec.rb
+++ b/spec/migrations/toggle_vsa_aggregations_enable_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ToggleVsaAggregationsEnable, :migration do
+RSpec.describe ToggleVsaAggregationsEnable, :migration, feature_category: :value_stream_management do
let(:aggregations) { table(:analytics_cycle_analytics_aggregations) }
let(:groups) { table(:namespaces) }
diff --git a/spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb b/spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb
index 842456089fe..d249fcecf66 100644
--- a/spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb
+++ b/spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe UpdateApplicationSettingsContainerRegistryExpPolWorkerCapacityDefault do
+RSpec.describe UpdateApplicationSettingsContainerRegistryExpPolWorkerCapacityDefault,
+feature_category: :container_registry do
let(:settings) { table(:application_settings) }
context 'with no rows in the application_settings table' do
diff --git a/spec/migrations/update_application_settings_protected_paths_spec.rb b/spec/migrations/update_application_settings_protected_paths_spec.rb
index 21879995f1b..d61eadf9f9c 100644
--- a/spec/migrations/update_application_settings_protected_paths_spec.rb
+++ b/spec/migrations/update_application_settings_protected_paths_spec.rb
@@ -3,12 +3,13 @@
require 'spec_helper'
require_migration!
-RSpec.describe UpdateApplicationSettingsProtectedPaths, :aggregate_failures do
+RSpec.describe UpdateApplicationSettingsProtectedPaths, :aggregate_failures,
+feature_category: :authentication_and_authorization do
subject(:migration) { described_class.new }
- let_it_be(:application_settings) { table(:application_settings) }
- let_it_be(:oauth_paths) { %w[/oauth/authorize /oauth/token] }
- let_it_be(:custom_paths) { %w[/foo /bar] }
+ let!(:application_settings) { table(:application_settings) }
+ let!(:oauth_paths) { %w[/oauth/authorize /oauth/token] }
+ let!(:custom_paths) { %w[/foo /bar] }
let(:default_paths) { application_settings.column_defaults.fetch('protected_paths') }
diff --git a/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb b/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb
index b73aa7016a1..ac7a4171063 100644
--- a/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb
+++ b/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe UpdateDefaultScanMethodOfDastSiteProfile do
+RSpec.describe UpdateDefaultScanMethodOfDastSiteProfile, feature_category: :dynamic_application_security_testing do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:dast_sites) { table(:dast_sites) }
diff --git a/spec/migrations/update_integrations_trigger_type_new_on_insert_spec.rb b/spec/migrations/update_integrations_trigger_type_new_on_insert_spec.rb
index 41cf35b40f4..efc051d9a68 100644
--- a/spec/migrations/update_integrations_trigger_type_new_on_insert_spec.rb
+++ b/spec/migrations/update_integrations_trigger_type_new_on_insert_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe UpdateIntegrationsTriggerTypeNewOnInsert do
+RSpec.describe UpdateIntegrationsTriggerTypeNewOnInsert, feature_category: :integrations do
let(:migration) { described_class.new }
let(:integrations) { table(:integrations) }
diff --git a/spec/migrations/update_invalid_member_states_spec.rb b/spec/migrations/update_invalid_member_states_spec.rb
index 802634230a9..6ae4b9f3c0f 100644
--- a/spec/migrations/update_invalid_member_states_spec.rb
+++ b/spec/migrations/update_invalid_member_states_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe UpdateInvalidMemberStates do
+RSpec.describe UpdateInvalidMemberStates, feature_category: :subgroups do
let(:members) { table(:members) }
let(:groups) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/update_invalid_web_hooks_spec.rb b/spec/migrations/update_invalid_web_hooks_spec.rb
index a65f82d7082..9e69d3637b8 100644
--- a/spec/migrations/update_invalid_web_hooks_spec.rb
+++ b/spec/migrations/update_invalid_web_hooks_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe UpdateInvalidWebHooks do
+RSpec.describe UpdateInvalidWebHooks, feature_category: :integrations do
let(:web_hooks) { table(:web_hooks) }
let(:groups) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/models/achievements/achievement_spec.rb b/spec/models/achievements/achievement_spec.rb
new file mode 100644
index 00000000000..10c04d184af
--- /dev/null
+++ b/spec/models/achievements/achievement_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Achievements::Achievement, type: :model, feature_category: :users do
+ describe 'associations' do
+ it { is_expected.to belong_to(:namespace).required }
+ end
+
+ describe 'modules' do
+ subject { described_class }
+
+ it { is_expected.to include_module(Avatarable) }
+ end
+
+ describe 'validations' do
+ subject { create(:achievement) }
+
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).case_insensitive.scoped_to([:namespace_id]) }
+ it { is_expected.to validate_length_of(:name).is_at_most(255) }
+ it { is_expected.to validate_length_of(:description).is_at_most(1024) }
+ end
+
+ describe '#name' do
+ it 'strips name' do
+ achievement = described_class.new(name: ' AchievementTest ')
+ achievement.valid?
+
+ expect(achievement.name).to eq('AchievementTest')
+ end
+ end
+end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 9d84279a75e..289408231a9 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe Appearance do
subject(:appearance) { described_class.new }
it { expect(appearance.title).to eq('') }
+ it { expect(appearance.short_title).to eq('') }
it { expect(appearance.description).to eq('') }
it { expect(appearance.new_project_guidelines).to eq('') }
it { expect(appearance.profile_image_guidelines).to eq('') }
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index fd86a784b2d..1454c82c531 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -125,6 +125,8 @@ RSpec.describe ApplicationSetting do
it { is_expected.to validate_numericality_of(:max_yaml_depth).only_integer.is_greater_than(0) }
it { is_expected.to validate_presence_of(:max_pages_size) }
it { is_expected.to validate_presence_of(:max_pages_custom_domains_per_project) }
+ it { is_expected.to validate_presence_of(:max_terraform_state_size_bytes) }
+ it { is_expected.to validate_numericality_of(:max_terraform_state_size_bytes).only_integer.is_greater_than_or_equal_to(0) }
it 'ensures max_pages_size is an integer greater than 0 (or equal to 0 to indicate unlimited/maximum)' do
is_expected.to validate_numericality_of(:max_pages_size).only_integer.is_greater_than_or_equal_to(0)
@@ -214,6 +216,10 @@ RSpec.describe ApplicationSetting do
it { is_expected.to allow_value(http).for(:jira_connect_proxy_url) }
it { is_expected.to allow_value(https).for(:jira_connect_proxy_url) }
+ it { is_expected.to allow_value(true).for(:bulk_import_enabled) }
+ it { is_expected.to allow_value(false).for(:bulk_import_enabled) }
+ it { is_expected.not_to allow_value(nil).for(:bulk_import_enabled) }
+
context 'when deactivate_dormant_users is enabled' do
before do
stub_application_setting(deactivate_dormant_users: true)
@@ -1283,11 +1289,10 @@ RSpec.describe ApplicationSetting do
end
end
- describe '#instance_review_permitted?', :request_store, :use_clean_rails_memory_store_caching do
+ describe '#instance_review_permitted?', :request_store, :use_clean_rails_memory_store_caching, :without_license do
subject { setting.instance_review_permitted? }
before do
- allow(License).to receive(:current).and_return(nil) if Gitlab.ee?
allow(Rails.cache).to receive(:fetch).and_call_original
expect(Rails.cache).to receive(:fetch).with('limited_users_count', anything).and_return(
::ApplicationSetting::INSTANCE_REVIEW_MIN_USERS + users_over_minimum
diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb
index f3c95332ca0..79a716c2087 100644
--- a/spec/models/badge_spec.rb
+++ b/spec/models/badge_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Badge do
- let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' }
+ let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{project_name}/%{default_branch}/%{commit_sha}/%{project_title}' }
describe 'validations' do
# Requires the let variable url_sym
@@ -64,7 +64,7 @@ RSpec.describe Badge do
it 'uses the project information to populate the url placeholders' do
stub_project_commit_info(project)
- expect(badge.public_send("rendered_#{method}", project)).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever"
+ expect(badge.public_send("rendered_#{method}", project)).to eq "http://www.example.com/#{project.full_path}/#{project.id}/#{project.path}/master/whatever/#{project.title}"
end
it 'returns the url if the project used is nil' do
diff --git a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
index 8d5c7ce84f6..d28fa0bbe97 100644
--- a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
+++ b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
@@ -14,224 +14,109 @@ RSpec.describe BlobViewer::MetricsDashboardYml do
subject(:viewer) { described_class.new(blob) }
- context 'with metrics_dashboard_exhaustive_validations feature flag on' do
- before do
- stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
- end
-
- context 'when the definition is valid' do
+ context 'when the definition is valid' do
+ describe '#valid?' do
before do
- allow(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).and_return([])
- end
-
- describe '#valid?' do
- it 'calls prepare! on the viewer' do
- expect(viewer).to receive(:prepare!)
-
- viewer.valid?
- end
-
- it 'processes dashboard yaml and returns true', :aggregate_failures do
- yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw!
-
- expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader|
- expect(loader).to receive(:load_raw!).and_call_original
- end
- expect(Gitlab::Metrics::Dashboard::Validator)
- .to receive(:errors)
- .with(yml, dashboard_path: '.gitlab/dashboards/custom-dashboard.yml', project: project)
- .and_call_original
- expect(viewer.valid?).to be true
- end
- end
-
- describe '#errors' do
- it 'returns empty array' do
- expect(viewer.errors).to eq []
- end
+ allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json)
end
- end
- context 'when definition is invalid' do
- let(:error) { ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new }
- let(:data) do
- <<~YAML
- dashboard:
- YAML
- end
+ it 'calls prepare! on the viewer' do
+ expect(viewer).to receive(:prepare!)
- before do
- allow(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).and_return([error])
+ viewer.valid?
end
- describe '#valid?' do
- it 'returns false' do
- expect(viewer.valid?).to be false
- end
- end
+ it 'processes dashboard yaml and returns true', :aggregate_failures do
+ yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw!
- describe '#errors' do
- it 'returns validation errors' do
- expect(viewer.errors).to eq ["Dashboard failed schema validation"]
+ expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader|
+ expect(loader).to receive(:load_raw!).and_call_original
end
+ expect(PerformanceMonitoring::PrometheusDashboard)
+ .to receive(:from_json)
+ .with(yml)
+ .and_call_original
+ expect(viewer.valid?).to be true
end
end
- context 'when YAML syntax is invalid' do
- let(:data) do
- <<~YAML
- dashboard: 'empty metrics'
- panel_groups:
- - group: 'Group Title'
- YAML
- end
-
- describe '#valid?' do
- it 'returns false' do
- expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors)
- expect(viewer.valid?).to be false
- end
- end
-
- describe '#errors' do
- it 'returns validation errors' do
- expect(viewer.errors).to all be_kind_of String
- end
- end
- end
-
- context 'when YAML loader raises error' do
- let(:data) do
- <<~YAML
- large yaml file
- YAML
- end
-
- before do
- allow(::Gitlab::Config::Loader::Yaml).to receive(:new)
- .and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
- end
-
- it 'is invalid' do
- expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors)
- expect(viewer.valid?).to be false
- end
-
- it 'returns validation errors' do
- expect(viewer.errors).to eq ['The parsed YAML is too big']
+ describe '#errors' do
+ it 'returns empty array' do
+ expect(viewer.errors).to eq []
end
end
end
- context 'with metrics_dashboard_exhaustive_validations feature flag off' do
- before do
- stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
+ context 'when definition is invalid' do
+ let(:error) { ActiveModel::ValidationError.new(PerformanceMonitoring::PrometheusDashboard.new.tap(&:validate)) }
+ let(:data) do
+ <<~YAML
+ dashboard:
+ YAML
end
- context 'when the definition is valid' do
- describe '#valid?' do
- before do
- allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json)
- end
-
- it 'calls prepare! on the viewer' do
- expect(viewer).to receive(:prepare!)
+ describe '#valid?' do
+ it 'returns false' do
+ expect(PerformanceMonitoring::PrometheusDashboard)
+ .to receive(:from_json).and_raise(error)
- viewer.valid?
- end
-
- it 'processes dashboard yaml and returns true', :aggregate_failures do
- yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw!
-
- expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader|
- expect(loader).to receive(:load_raw!).and_call_original
- end
- expect(PerformanceMonitoring::PrometheusDashboard)
- .to receive(:from_json)
- .with(yml)
- .and_call_original
- expect(viewer.valid?).to be true
- end
- end
-
- describe '#errors' do
- it 'returns empty array' do
- expect(viewer.errors).to eq []
- end
+ expect(viewer.valid?).to be false
end
end
- context 'when definition is invalid' do
- let(:error) { ActiveModel::ValidationError.new(PerformanceMonitoring::PrometheusDashboard.new.tap(&:validate)) }
- let(:data) do
- <<~YAML
- dashboard:
- YAML
- end
-
- describe '#valid?' do
- it 'returns false' do
- expect(PerformanceMonitoring::PrometheusDashboard)
- .to receive(:from_json).and_raise(error)
+ describe '#errors' do
+ it 'returns validation errors' do
+ allow(PerformanceMonitoring::PrometheusDashboard)
+ .to receive(:from_json).and_raise(error)
- expect(viewer.valid?).to be false
- end
- end
-
- describe '#errors' do
- it 'returns validation errors' do
- allow(PerformanceMonitoring::PrometheusDashboard)
- .to receive(:from_json).and_raise(error)
-
- expect(viewer.errors).to eq error.model.errors.messages.map { |messages| messages.join(': ') }
- end
+ expect(viewer.errors).to eq error.model.errors.messages.map { |messages| messages.join(': ') }
end
end
+ end
- context 'when YAML syntax is invalid' do
- let(:data) do
- <<~YAML
+ context 'when YAML syntax is invalid' do
+ let(:data) do
+ <<~YAML
dashboard: 'empty metrics'
panel_groups:
- group: 'Group Title'
- YAML
- end
+ YAML
+ end
- describe '#valid?' do
- it 'returns false' do
- expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
- expect(viewer.valid?).to be false
- end
+ describe '#valid?' do
+ it 'returns false' do
+ expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
+ expect(viewer.valid?).to be false
end
+ end
- describe '#errors' do
- it 'returns validation errors' do
- expect(viewer.errors).to eq ["YAML syntax: (<unknown>): did not find expected key while parsing a block mapping at line 1 column 1"]
- end
+ describe '#errors' do
+ it 'returns validation errors' do
+ expect(viewer.errors).to eq ["YAML syntax: (<unknown>): did not find expected key while parsing a block mapping at line 1 column 1"]
end
end
+ end
- context 'when YAML loader raises error' do
- let(:data) do
- <<~YAML
+ context 'when YAML loader raises error' do
+ let(:data) do
+ <<~YAML
large yaml file
- YAML
- end
+ YAML
+ end
- before do
- allow(::Gitlab::Config::Loader::Yaml).to(
- receive(:new).and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
- )
- end
+ before do
+ allow(::Gitlab::Config::Loader::Yaml).to(
+ receive(:new).and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big')
+ )
+ end
- it 'is invalid' do
- expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
- expect(viewer.valid?).to be false
- end
+ it 'is invalid' do
+ expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json)
+ expect(viewer.valid?).to be false
+ end
- it 'returns validation errors' do
- expect(viewer.errors).to eq ["YAML syntax: The parsed YAML is too big"]
- end
+ it 'returns validation errors' do
+ expect(viewer.errors).to eq ["YAML syntax: The parsed YAML is too big"]
end
end
end
diff --git a/spec/models/bulk_imports/tracker_spec.rb b/spec/models/bulk_imports/tracker_spec.rb
index 1aa76d4dadd..1516ab106cb 100644
--- a/spec/models/bulk_imports/tracker_spec.rb
+++ b/spec/models/bulk_imports/tracker_spec.rb
@@ -54,13 +54,16 @@ RSpec.describe BulkImports::Tracker, type: :model do
it 'returns the not started pipeline trackers from the minimum stage number' do
stage_1_tracker = create(:bulk_import_tracker, entity: entity, stage: 1)
+ stage_1_finished_tracker = create(:bulk_import_tracker, :finished, entity: entity, stage: 1)
+ stage_1_failed_tracker = create(:bulk_import_tracker, :failed, entity: entity, stage: 1)
+ stage_1_skipped_tracker = create(:bulk_import_tracker, :skipped, entity: entity, stage: 1)
stage_2_tracker = create(:bulk_import_tracker, entity: entity, stage: 2)
expect(described_class.next_pipeline_trackers_for(entity.id))
.to include(stage_1_tracker)
expect(described_class.next_pipeline_trackers_for(entity.id))
- .not_to include(stage_2_tracker)
+ .not_to include(stage_2_tracker, stage_1_finished_tracker, stage_1_failed_tracker, stage_1_skipped_tracker)
end
end
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index df24c92149d..169b00b9c74 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Bridge do
+RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let_it_be(:target_project) { create(:project, name: 'project', namespace: create(:namespace, name: 'my')) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
@@ -34,6 +34,24 @@ RSpec.describe Ci::Bridge do
expect(bridge).to have_one(:downstream_pipeline)
end
+ describe '#sourced_pipelines' do
+ subject { bridge.sourced_pipelines }
+
+ it 'raises error' do
+ expect { subject }.to raise_error RuntimeError, 'Ci::Bridge does not have sourced_pipelines association'
+ end
+
+ context 'when ci_bridge_remove_sourced_pipelines is disabled' do
+ before do
+ stub_feature_flags(ci_bridge_remove_sourced_pipelines: false)
+ end
+
+ it 'returns the sourced_pipelines association' do
+ expect(bridge.sourced_pipelines).to eq([])
+ end
+ end
+ end
+
describe '#retryable?' do
let(:bridge) { create(:ci_bridge, :success) }
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index e728ce0f474..8bf3af44be6 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -6,7 +6,6 @@ RSpec.describe Ci::BuildMetadata do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group, build_timeout: 2000) }
-
let_it_be(:pipeline) do
create(:ci_pipeline, project: project,
sha: project.commit.id,
@@ -14,7 +13,9 @@ RSpec.describe Ci::BuildMetadata do
status: 'success')
end
- let(:job) { create(:ci_build, pipeline: pipeline) }
+ let_it_be_with_reload(:runner) { create(:ci_runner) }
+
+ let(:job) { create(:ci_build, pipeline: pipeline, runner: runner) }
let(:metadata) { job.metadata }
it_behaves_like 'having unique enum values'
@@ -32,63 +33,110 @@ RSpec.describe Ci::BuildMetadata do
end
end
- context 'when project timeout is set' do
- context 'when runner is assigned to the job' do
+ context 'when job, project and runner timeouts are set' do
+ context 'when job timeout is lower then runner timeout' do
before do
- job.update!(runner: runner)
+ runner.update!(maximum_timeout: 4000)
+ job.update!(options: { job_timeout: 3000 })
end
- context 'when runner timeout is not set' do
- let(:runner) { create(:ci_runner, maximum_timeout: nil) }
+ it_behaves_like 'sets timeout', 'job_timeout_source', 3000
+ end
- it_behaves_like 'sets timeout', 'project_timeout_source', 2000
+ context 'when runner timeout is lower then job timeout' do
+ before do
+ runner.update!(maximum_timeout: 2000)
+ job.update!(options: { job_timeout: 3000 })
end
- context 'when runner timeout is lower than project timeout' do
- let(:runner) { create(:ci_runner, maximum_timeout: 1900) }
+ it_behaves_like 'sets timeout', 'runner_timeout_source', 2000
+ end
+ end
- it_behaves_like 'sets timeout', 'runner_timeout_source', 1900
+ context 'when job, project timeout values are set and runner is assigned' do
+ context 'when runner has no timeout set' do
+ before do
+ runner.update!(maximum_timeout: nil)
+ job.update!(options: { job_timeout: 3000 })
end
- context 'when runner timeout is higher than project timeout' do
- let(:runner) { create(:ci_runner, maximum_timeout: 2100) }
+ it_behaves_like 'sets timeout', 'job_timeout_source', 3000
+ end
+ end
- it_behaves_like 'sets timeout', 'project_timeout_source', 2000
+ context 'when only job and project timeouts are defined' do
+ context 'when job timeout is lower then project timeout' do
+ before do
+ job.update!(options: { job_timeout: 1000 })
end
- end
- context 'when job timeout is set' do
- context 'when job timeout is higher than project timeout' do
- let(:job) { create(:ci_build, pipeline: pipeline, options: { job_timeout: 3000 }) }
+ it_behaves_like 'sets timeout', 'job_timeout_source', 1000
+ end
- it_behaves_like 'sets timeout', 'job_timeout_source', 3000
+ context 'when project timeout is lower then job timeout' do
+ before do
+ job.update!(options: { job_timeout: 3000 })
end
- context 'when job timeout is lower than project timeout' do
- let(:job) { create(:ci_build, pipeline: pipeline, options: { job_timeout: 1000 }) }
+ it_behaves_like 'sets timeout', 'job_timeout_source', 3000
+ end
+ end
+
+ context 'when only project and runner timeouts are defined' do
+ before do
+ runner.update!(maximum_timeout: 1900)
+ end
+
+ context 'when runner timeout is lower then project timeout' do
+ it_behaves_like 'sets timeout', 'runner_timeout_source', 1900
+ end
- it_behaves_like 'sets timeout', 'job_timeout_source', 1000
+ context 'when project timeout is lower then runner timeout' do
+ before do
+ runner.update!(maximum_timeout: 2100)
end
+
+ it_behaves_like 'sets timeout', 'project_timeout_source', 2000
end
+ end
- context 'when both runner and job timeouts are set' do
+ context 'when only job and runner timeouts are defined' do
+ context 'when runner timeout is lower them job timeout' do
before do
- job.update!(runner: runner)
+ job.update!(options: { job_timeout: 2000 })
+ runner.update!(maximum_timeout: 1900)
end
- context 'when job timeout is higher than runner timeout' do
- let(:job) { create(:ci_build, pipeline: pipeline, options: { job_timeout: 3000 }) }
- let(:runner) { create(:ci_runner, maximum_timeout: 2100) }
+ it_behaves_like 'sets timeout', 'runner_timeout_source', 1900
+ end
- it_behaves_like 'sets timeout', 'runner_timeout_source', 2100
+ context 'when job timeout is lower them runner timeout' do
+ before do
+ job.update!(options: { job_timeout: 1000 })
+ runner.update!(maximum_timeout: 1900)
end
- context 'when job timeout is lower than runner timeout' do
- let(:job) { create(:ci_build, pipeline: pipeline, options: { job_timeout: 1900 }) }
- let(:runner) { create(:ci_runner, maximum_timeout: 2100) }
+ it_behaves_like 'sets timeout', 'job_timeout_source', 1000
+ end
+ end
+
+ context 'when only job timeout is defined and runner is assigned, but has no timeout set' do
+ before do
+ job.update!(options: { job_timeout: 1000 })
+ runner.update!(maximum_timeout: nil)
+ end
+
+ it_behaves_like 'sets timeout', 'job_timeout_source', 1000
+ end
- it_behaves_like 'sets timeout', 'job_timeout_source', 1900
+ context 'when only one timeout value is defined' do
+ context 'when only project timeout value is defined' do
+ before do
+ job.update!(options: { job_timeout: nil })
+ runner.update!(maximum_timeout: nil)
end
+
+ it_behaves_like 'sets timeout', 'project_timeout_source', 2000
end
end
end
@@ -107,9 +155,7 @@ RSpec.describe Ci::BuildMetadata do
}
metadata.id_tokens = {
TEST_JWT_TOKEN: {
- id_token: {
- aud: 'https://gitlab.test'
- }
+ aud: 'https://gitlab.test'
}
}
@@ -152,6 +198,29 @@ RSpec.describe Ci::BuildMetadata do
end
end
+ describe '#enable_debug_trace!' do
+ subject { metadata.enable_debug_trace! }
+
+ context 'when debug_trace_enabled is false' do
+ it 'sets debug_trace_enabled to true' do
+ subject
+
+ expect(metadata.debug_trace_enabled).to eq(true)
+ end
+ end
+
+ context 'when debug_trace_enabled is true' do
+ before do
+ metadata.update!(debug_trace_enabled: true)
+ end
+
+ it 'does not set debug_trace_enabled to true', :aggregate_failures do
+ expect(described_class).not_to receive(:save!)
+ expect(metadata.debug_trace_enabled).to eq(true)
+ end
+ end
+ end
+
describe 'partitioning' do
context 'with job' do
let(:status) { build(:commit_status, partition_id: 123) }
@@ -183,28 +252,6 @@ RSpec.describe Ci::BuildMetadata do
end
end
- describe 'routing table switch' do
- context 'with ff disabled' do
- before do
- stub_feature_flags(ci_partitioning_use_ci_builds_metadata_routing_table: false)
- end
-
- it 'uses the legacy table' do
- expect(described_class.table_name).to eq('ci_builds_metadata')
- end
- end
-
- context 'with ff enabled' do
- before do
- stub_feature_flags(ci_partitioning_use_ci_builds_metadata_routing_table: true)
- end
-
- it 'uses the routing table' do
- expect(described_class.table_name).to eq('p_ci_builds_metadata')
- end
- end
- end
-
context 'jsonb fields serialization' do
it 'changing other fields does not change config_options' do
expect { metadata.id = metadata.id }.not_to change(metadata, :changes)
diff --git a/spec/models/ci/build_need_spec.rb b/spec/models/ci/build_need_spec.rb
index c2cf9027055..aa1c57d1788 100644
--- a/spec/models/ci/build_need_spec.rb
+++ b/spec/models/ci/build_need_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildNeed, model: true do
+RSpec.describe Ci::BuildNeed, model: true, feature_category: :continuous_integration do
let(:build_need) { build(:ci_build_need) }
it { is_expected.to belong_to(:build).class_name('Ci::Processable') }
@@ -35,4 +35,62 @@ RSpec.describe Ci::BuildNeed, model: true do
end
end
end
+
+ describe 'partitioning' do
+ context 'with build' do
+ let(:build) { FactoryBot.build(:ci_build, partition_id: ci_testing_partition_id) }
+ let(:build_need) { FactoryBot.build(:ci_build_need, build: build) }
+
+ it 'sets partition_id to the current partition value' do
+ expect { build_need.valid? }.to change { build_need.partition_id }.to(ci_testing_partition_id)
+ end
+
+ context 'when it is already set' do
+ let(:build_need) { FactoryBot.build(:ci_build_need, partition_id: 125) }
+
+ it 'does not change the partition_id value' do
+ expect { build_need.valid? }.not_to change { build_need.partition_id }
+ end
+ end
+ end
+
+ context 'without build' do
+ let(:build_need) { FactoryBot.build(:ci_build_need, build: nil) }
+
+ it { is_expected.to validate_presence_of(:partition_id) }
+
+ it 'does not change the partition_id value' do
+ expect { build_need.valid? }.not_to change { build_need.partition_id }
+ end
+ end
+
+ context 'when using bulk_insert' do
+ include Ci::PartitioningHelpers
+
+ let(:new_pipeline) { create(:ci_pipeline) }
+ let(:ci_build) { build(:ci_build, pipeline: new_pipeline) }
+
+ before do
+ stub_current_partition_id
+ end
+
+ it 'creates build needs successfully', :aggregate_failures do
+ ci_build.needs_attributes = [
+ { name: "build", artifacts: true },
+ { name: "build2", artifacts: true },
+ { name: "build3", artifacts: true }
+ ]
+
+ expect(described_class).to receive(:bulk_insert!).and_call_original
+
+ BulkInsertableAssociations.with_bulk_insert do
+ ci_build.save!
+ end
+
+ expect(described_class.count).to eq(3)
+ expect(described_class.first.partition_id).to eq(ci_testing_partition_id)
+ expect(described_class.second.partition_id).to eq(ci_testing_partition_id)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_pending_state_spec.rb b/spec/models/ci/build_pending_state_spec.rb
index a546d2aff65..756180621ec 100644
--- a/spec/models/ci/build_pending_state_spec.rb
+++ b/spec/models/ci/build_pending_state_spec.rb
@@ -24,4 +24,33 @@ RSpec.describe Ci::BuildPendingState do
end
end
end
+
+ describe 'partitioning' do
+ context 'with build' do
+ let(:build) { FactoryBot.build(:ci_build, partition_id: ci_testing_partition_id) }
+ let(:build_pending_state) { FactoryBot.build(:ci_build_pending_state, build: build) }
+
+ it 'sets partition_id to the current partition value' do
+ expect { build_pending_state.valid? }.to change { build_pending_state.partition_id }.to(ci_testing_partition_id)
+ end
+
+ context 'when it is already set' do
+ let(:build_pending_state) { FactoryBot.build(:ci_build_pending_state, partition_id: 125) }
+
+ it 'does not change the partition_id value' do
+ expect { build_pending_state.valid? }.not_to change { build_pending_state.partition_id }
+ end
+ end
+ end
+
+ context 'without build' do
+ let(:build_pending_state) { FactoryBot.build(:ci_build_pending_state, build: nil) }
+
+ it { is_expected.to validate_presence_of(:partition_id) }
+
+ it 'does not change the partition_id value' do
+ expect { build_pending_state.valid? }.not_to change { build_pending_state.partition_id }
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_report_result_spec.rb b/spec/models/ci/build_report_result_spec.rb
index 09ea19cf077..90b23d3e824 100644
--- a/spec/models/ci/build_report_result_spec.rb
+++ b/spec/models/ci/build_report_result_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::BuildReportResult do
- let(:build_report_result) { build(:ci_build_report_result, :with_junit_success) }
+ let_it_be_with_reload(:build_report_result) { create(:ci_build_report_result, :with_junit_success) }
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:project) }
@@ -70,4 +70,34 @@ RSpec.describe Ci::BuildReportResult do
expect(build_report_result.tests_skipped).to eq(0)
end
end
+
+ describe 'partitioning' do
+ let(:build_report_result) { FactoryBot.build(:ci_build_report_result, build: build) }
+
+ context 'with build' do
+ let(:build) { FactoryBot.build(:ci_build, partition_id: ci_testing_partition_id) }
+
+ it 'copies the partition_id from build' do
+ expect { build_report_result.valid? }.to change { build_report_result.partition_id }.to(ci_testing_partition_id)
+ end
+
+ context 'when it is already set' do
+ let(:build_report_result) { FactoryBot.build(:ci_build_report_result, partition_id: 125) }
+
+ it 'does not change the partition_id value' do
+ expect { build_report_result.valid? }.not_to change { build_report_result.partition_id }
+ end
+ end
+ end
+
+ context 'without build' do
+ subject(:build_report_result) { FactoryBot.build(:ci_build_report_result, build: nil, partition_id: 125) }
+
+ it { is_expected.to validate_presence_of(:partition_id) }
+
+ it 'does not change the partition_id value' do
+ expect { build_report_result.valid? }.not_to change { build_report_result.partition_id }
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_runner_session_spec.rb b/spec/models/ci/build_runner_session_spec.rb
index 8dfe854511c..5e1a489ed8b 100644
--- a/spec/models/ci/build_runner_session_spec.rb
+++ b/spec/models/ci/build_runner_session_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildRunnerSession, model: true do
+RSpec.describe Ci::BuildRunnerSession, model: true, feature_category: :continuous_integration do
let!(:build) { create(:ci_build, :with_runner_session) }
let(:url) { 'https://new.example.com' }
@@ -174,4 +174,20 @@ RSpec.describe Ci::BuildRunnerSession, model: true do
end
end
end
+
+ describe 'partitioning' do
+ include Ci::PartitioningHelpers
+
+ let(:new_pipeline) { create(:ci_pipeline) }
+ let(:new_build) { create(:ci_build, pipeline: new_pipeline) }
+ let(:build_runner_session) { create(:ci_build_runner_session, build: new_build) }
+
+ before do
+ stub_current_partition_id
+ end
+
+ it 'assigns the same partition id as the one that build has' do
+ expect(build_runner_session.partition_id).to eq(ci_testing_partition_id)
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 813b4b3faa6..c978e33bf54 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Build do
+RSpec.describe Ci::Build, feature_category: :continuous_integration do
include Ci::TemplateHelpers
include AfterNextHelpers
@@ -1754,8 +1754,8 @@ RSpec.describe Ci::Build do
end
end
- describe '#starts_environment?' do
- subject { build.starts_environment? }
+ describe '#deployment_job?' do
+ subject { build.deployment_job? }
context 'when environment is defined' do
before do
@@ -2528,20 +2528,24 @@ RSpec.describe Ci::Build do
end
describe '#ref_slug' do
- {
- 'master' => 'master',
- '1-foo' => '1-foo',
- 'fix/1-foo' => 'fix-1-foo',
- 'fix-1-foo' => 'fix-1-foo',
- 'a' * 63 => 'a' * 63,
- 'a' * 64 => 'a' * 63,
- 'FOO' => 'foo',
- '-' + 'a' * 61 + '-' => 'a' * 61,
- '-' + 'a' * 62 + '-' => 'a' * 62,
- '-' + 'a' * 63 + '-' => 'a' * 62,
- 'a' * 62 + ' ' => 'a' * 62
- }.each do |ref, slug|
- it "transforms #{ref} to #{slug}" do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ref, :slug) do
+ 'master' | 'master'
+ '1-foo' | '1-foo'
+ 'fix/1-foo' | 'fix-1-foo'
+ 'fix-1-foo' | 'fix-1-foo'
+ 'a' * 63 | 'a' * 63
+ 'a' * 64 | 'a' * 63
+ 'FOO' | 'foo'
+ '-' + 'a' * 61 + '-' | 'a' * 61
+ '-' + 'a' * 62 + '-' | 'a' * 62
+ '-' + 'a' * 63 + '-' | 'a' * 62
+ 'a' * 62 + ' ' | 'a' * 62
+ end
+
+ with_them do
+ it "transforms ref to slug" do
build.ref = ref
expect(build.ref_slug).to eq(slug)
@@ -2737,6 +2741,7 @@ RSpec.describe Ci::Build do
{ key: 'CI_PROJECT_PATH', value: project.full_path, public: true, masked: false },
{ key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true, masked: false },
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true, masked: false },
+ { key: 'CI_PROJECT_NAMESPACE_ID', value: project.namespace.id.to_s, public: true, masked: false },
{ key: 'CI_PROJECT_ROOT_NAMESPACE', value: project.namespace.root_ancestor.path, public: true, masked: false },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true, masked: false },
{ key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true, masked: false },
@@ -2883,6 +2888,9 @@ RSpec.describe Ci::Build do
value: 'var',
public: true }]
build.environment = 'staging'
+
+ # CI_ENVIRONMENT_NAME is set in predefined_variables when job environment is provided
+ predefined_variables.insert(20, { key: 'CI_ENVIRONMENT_NAME', value: 'staging', public: true, masked: false })
end
it 'matches explicit variables ordering' do
@@ -3539,8 +3547,8 @@ RSpec.describe Ci::Build do
rsa_key = OpenSSL::PKey::RSA.generate(3072).to_s
stub_application_setting(ci_jwt_signing_key: rsa_key)
build.metadata.update!(id_tokens: {
- 'ID_TOKEN_1' => { id_token: { aud: 'developers' } },
- 'ID_TOKEN_2' => { id_token: { aud: 'maintainers' } }
+ 'ID_TOKEN_1' => { aud: 'developers' },
+ 'ID_TOKEN_2' => { aud: 'maintainers' }
})
end
@@ -3817,22 +3825,6 @@ RSpec.describe Ci::Build do
it 'assigns the token' do
expect { build.enqueue }.to change(build, :token).from(nil).to(an_instance_of(String))
end
-
- context 'with ci_assign_job_token_on_scheduling disabled' do
- before do
- stub_feature_flags(ci_assign_job_token_on_scheduling: false)
- end
-
- it 'assigns the token on creation' do
- expect(build.token).to be_present
- end
-
- it 'does not change the token when enqueuing' do
- expect { build.enqueue }.not_to change(build, :token)
-
- expect(build).to be_pending
- end
- end
end
describe 'state transition: pending: :running' do
@@ -5442,7 +5434,7 @@ RSpec.describe Ci::Build do
it 'delegates to Ci::BuildTraceMetadata' do
expect(Ci::BuildTraceMetadata)
.to receive(:find_or_upsert_for!)
- .with(build.id)
+ .with(build.id, build.partition_id)
build.ensure_trace_metadata!
end
@@ -5617,4 +5609,72 @@ RSpec.describe Ci::Build do
end
end
end
+
+ describe '#runtime_hooks' do
+ let(:build1) do
+ FactoryBot.build(:ci_build,
+ options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } })
+ end
+
+ subject(:runtime_hooks) { build1.runtime_hooks }
+
+ it 'returns an array of hook objects' do
+ expect(runtime_hooks.size).to eq(1)
+ expect(runtime_hooks[0].name).to eq('pre_get_sources_script')
+ expect(runtime_hooks[0].script).to eq(["echo 'hello pre_get_sources_script'"])
+ end
+ end
+
+ describe 'partitioning', :ci_partitionable do
+ include Ci::PartitioningHelpers
+
+ let(:new_pipeline) { create(:ci_pipeline) }
+ let(:ci_build) { FactoryBot.build(:ci_build, pipeline: new_pipeline) }
+
+ before do
+ stub_current_partition_id
+ end
+
+ it 'assigns partition_id to job variables successfully', :aggregate_failures do
+ ci_build.job_variables_attributes = [
+ { key: 'TEST_KEY', value: 'new value' },
+ { key: 'NEW_KEY', value: 'exciting new value' }
+ ]
+
+ ci_build.save!
+
+ expect(ci_build.job_variables.count).to eq(2)
+ expect(ci_build.job_variables.first.partition_id).to eq(ci_testing_partition_id)
+ expect(ci_build.job_variables.second.partition_id).to eq(ci_testing_partition_id)
+ end
+ end
+
+ describe 'assigning token', :ci_partitionable do
+ include Ci::PartitioningHelpers
+
+ let(:new_pipeline) { create(:ci_pipeline) }
+ let(:ci_build) { create(:ci_build, pipeline: new_pipeline) }
+
+ before do
+ stub_current_partition_id
+ end
+
+ it 'includes partition_id as a token prefix' do
+ prefix = ci_build.token.split('_').first.to_i(16)
+
+ expect(prefix).to eq(ci_testing_partition_id)
+ end
+
+ context 'when ci_build_partition_id_token_prefix is disabled' do
+ before do
+ stub_feature_flags(ci_build_partition_id_token_prefix: false)
+ end
+
+ it 'does not include partition_id as a token prefix' do
+ prefix = ci_build.token.split('_').first.to_i(16)
+
+ expect(prefix).not_to eq(ci_testing_partition_id)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index 3328ed62f15..ac0a18a176d 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -15,13 +15,13 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
described_class.new(build: build, chunk_index: chunk_index, data_store: data_store, raw_data: raw_data)
end
- it_behaves_like 'having unique enum values'
-
before do
stub_feature_flags(ci_enable_live_trace: true)
stub_artifacts_object_storage
end
+ it_behaves_like 'having unique enum values'
+
def redis_instance
{
redis: Gitlab::Redis::SharedState,
@@ -954,4 +954,33 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
it { is_expected.to eq(value) }
end
end
+
+ describe 'partitioning' do
+ context 'with build' do
+ let(:build) { FactoryBot.build(:ci_build, partition_id: ci_testing_partition_id) }
+ let(:build_trace_chunk) { FactoryBot.build(:ci_build_trace_chunk, build: build) }
+
+ it 'sets partition_id to the current partition value' do
+ expect { build_trace_chunk.valid? }.to change { build_trace_chunk.partition_id }.to(ci_testing_partition_id)
+ end
+
+ context 'when it is already set' do
+ let(:build_trace_chunk) { FactoryBot.build(:ci_build_trace_chunk, partition_id: 125) }
+
+ it 'does not change the partition_id value' do
+ expect { build_trace_chunk.valid? }.not_to change { build_trace_chunk.partition_id }
+ end
+ end
+ end
+
+ context 'without build' do
+ let(:build_trace_chunk) { FactoryBot.build(:ci_build_trace_chunk, build: nil, partition_id: 125) }
+
+ it { is_expected.to validate_presence_of(:partition_id) }
+
+ it 'does not change the partition_id value' do
+ expect { build_trace_chunk.valid? }.not_to change { build_trace_chunk.partition_id }
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_trace_metadata_spec.rb b/spec/models/ci/build_trace_metadata_spec.rb
index 120e4289da2..2ab300e4054 100644
--- a/spec/models/ci/build_trace_metadata_spec.rb
+++ b/spec/models/ci/build_trace_metadata_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildTraceMetadata do
+RSpec.describe Ci::BuildTraceMetadata, feature_category: :continuous_integration do
it { is_expected.to belong_to(:build) }
it { is_expected.to belong_to(:trace_artifact) }
@@ -106,7 +106,7 @@ RSpec.describe Ci::BuildTraceMetadata do
let_it_be(:build) { create(:ci_build) }
subject(:execute) do
- described_class.find_or_upsert_for!(build.id)
+ described_class.find_or_upsert_for!(build.id, build.partition_id)
end
it 'creates a new record' do
@@ -158,4 +158,22 @@ RSpec.describe Ci::BuildTraceMetadata do
it { is_expected.to eq(result) }
end
end
+
+ describe 'partitioning' do
+ include Ci::PartitioningHelpers
+
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:new_pipeline) { create(:ci_pipeline) }
+ let(:new_build) { create(:ci_build, pipeline: new_pipeline) }
+ let(:metadata) { create(:ci_build_trace_metadata, build: new_build) }
+
+ before do
+ stub_current_partition_id
+ end
+
+ it 'assigns the same partition id as the one that build has' do
+ expect(metadata.partition_id).to eq(ci_testing_partition_id)
+ end
+ end
end
diff --git a/spec/models/ci/freeze_period_spec.rb b/spec/models/ci/freeze_period_spec.rb
index b9bf1657e28..d8add736d6a 100644
--- a/spec/models/ci/freeze_period_spec.rb
+++ b/spec/models/ci/freeze_period_spec.rb
@@ -2,16 +2,22 @@
require 'spec_helper'
-RSpec.describe Ci::FreezePeriod, type: :model do
+RSpec.describe Ci::FreezePeriod, feature_category: :release_orchestration, type: :model do
+ let_it_be(:project) { create(:project) }
+
+ # Freeze period factory is on a weekend, so we travel in time, in and around that.
+ let(:friday_2300_time) { Time.utc(2020, 4, 10, 23, 0) }
+ let(:saturday_1200_time) { Time.utc(2020, 4, 11, 12, 0) }
+ let(:monday_0700_time) { Time.utc(2020, 4, 13, 7, 0) }
+ let(:tuesday_0800_time) { Time.utc(2020, 4, 14, 8, 0) }
+
subject { build(:ci_freeze_period) }
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:project) }
- let!(:model) { create(:ci_freeze_period, project: parent) }
+ let!(:model) { create(:ci_freeze_period, project: parent) }
end
- let(:invalid_cron) { '0 0 0 * *' }
-
it { is_expected.to belong_to(:project) }
it { is_expected.to respond_to(:freeze_start) }
@@ -19,37 +25,142 @@ RSpec.describe Ci::FreezePeriod, type: :model do
it { is_expected.to respond_to(:cron_timezone) }
describe 'cron validations' do
+ let(:invalid_cron) { '0 0 0 * *' }
+
it 'allows valid cron patterns' do
- freeze_period = build(:ci_freeze_period)
+ freeze_period = build_stubbed(:ci_freeze_period)
expect(freeze_period).to be_valid
end
it 'does not allow invalid cron patterns on freeze_start' do
- freeze_period = build(:ci_freeze_period, freeze_start: invalid_cron)
+ freeze_period = build_stubbed(:ci_freeze_period, freeze_start: invalid_cron)
expect(freeze_period).not_to be_valid
end
it 'does not allow invalid cron patterns on freeze_end' do
- freeze_period = build(:ci_freeze_period, freeze_end: invalid_cron)
+ freeze_period = build_stubbed(:ci_freeze_period, freeze_end: invalid_cron)
expect(freeze_period).not_to be_valid
end
it 'does not allow an invalid timezone' do
- freeze_period = build(:ci_freeze_period, cron_timezone: 'invalid')
+ freeze_period = build_stubbed(:ci_freeze_period, cron_timezone: 'invalid')
expect(freeze_period).not_to be_valid
end
context 'when cron contains trailing whitespaces' do
it 'strips the attribute' do
- freeze_period = build(:ci_freeze_period, freeze_start: ' 0 0 * * * ')
+ freeze_period = build_stubbed(:ci_freeze_period, freeze_start: ' 0 0 * * * ')
expect(freeze_period).to be_valid
expect(freeze_period.freeze_start).to eq('0 0 * * *')
end
end
end
+
+ shared_examples 'within freeze period' do |time|
+ it 'is frozen' do
+ travel_to(time) do
+ expect(subject).to eq(Ci::FreezePeriod::STATUS_ACTIVE)
+ end
+ end
+ end
+
+ shared_examples 'outside freeze period' do |time|
+ it 'is not frozen' do
+ travel_to(time) do
+ expect(subject).to eq(Ci::FreezePeriod::STATUS_INACTIVE)
+ end
+ end
+ end
+
+ describe '#status' do
+ subject { freeze_period.status }
+
+ describe 'single freeze period' do
+ let(:freeze_period) do
+ build_stubbed(:ci_freeze_period, project: project)
+ end
+
+ it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59)
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1)
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59)
+ it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 7, 1)
+ end
+
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/370472
+ context 'when period overlaps with itself' do
+ let(:freeze_period) do
+ build_stubbed(:ci_freeze_period, project: project, freeze_start: '* * * 8 *', freeze_end: '* * * 10 *')
+ end
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 8, 11, 0, 0)
+ it_behaves_like 'outside freeze period', Time.utc(2020, 10, 11, 0, 0)
+ end
+ end
+
+ shared_examples 'a freeze period method' do
+ let(:freeze_period) { build_stubbed(:ci_freeze_period, project: project) }
+
+ it 'returns the correct value' do
+ travel_to(now) do
+ expect(freeze_period.send(method)).to eq(expected)
+ end
+ end
+ end
+
+ describe '#active?' do
+ context 'when freeze period status is active' do
+ it_behaves_like 'a freeze period method' do
+ let(:now) { saturday_1200_time }
+ let(:method) { :active? }
+ let(:expected) { true }
+ end
+ end
+
+ context 'when freeze period status is inactive' do
+ it_behaves_like 'a freeze period method' do
+ let(:now) { tuesday_0800_time }
+ let(:method) { :active? }
+ let(:expected) { false }
+ end
+ end
+ end
+
+ describe '#time_start' do
+ it_behaves_like 'a freeze period method' do
+ let(:now) { monday_0700_time }
+ let(:method) { :time_start }
+ let(:expected) { friday_2300_time }
+ end
+ end
+
+ describe '#next_time_start' do
+ let(:next_friday_2300_time) { Time.utc(2020, 4, 17, 23, 0) }
+
+ it_behaves_like 'a freeze period method' do
+ let(:now) { monday_0700_time }
+ let(:method) { :next_time_start }
+ let(:expected) { next_friday_2300_time }
+ end
+ end
+
+ describe '#time_end_from_now' do
+ it_behaves_like 'a freeze period method' do
+ let(:now) { saturday_1200_time }
+ let(:method) { :time_end_from_now }
+ let(:expected) { monday_0700_time }
+ end
+ end
+
+ describe '#time_end_from_start' do
+ it_behaves_like 'a freeze period method' do
+ let(:now) { saturday_1200_time }
+ let(:method) { :time_end_from_start }
+ let(:expected) { monday_0700_time }
+ end
+ end
end
diff --git a/spec/models/ci/freeze_period_status_spec.rb b/spec/models/ci/freeze_period_status_spec.rb
deleted file mode 100644
index ecbb7af64f7..00000000000
--- a/spec/models/ci/freeze_period_status_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Ci::FreezePeriodStatus do
- let(:project) { create :project }
- # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday.""
- let(:friday_2300) { '0 23 * * 5' }
- let(:monday_0700) { '0 7 * * 1' }
-
- subject { described_class.new(project: project).execute }
-
- shared_examples 'within freeze period' do |time|
- it 'is frozen' do
- travel_to(time) do
- expect(subject).to be_truthy
- end
- end
- end
-
- shared_examples 'outside freeze period' do |time|
- it 'is not frozen' do
- travel_to(time) do
- expect(subject).to be_falsy
- end
- end
- end
-
- describe 'single freeze period' do
- let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) }
-
- it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59)
-
- it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 7, 1)
- end
-
- describe 'multiple freeze periods' do
- # '30 23 * * 5' == "At 23:30 on Friday."", '0 8 * * 1' == "At 08:00 on Monday.""
- let(:friday_2330) { '30 23 * * 5' }
- let(:monday_0800) { '0 8 * * 1' }
-
- let!(:freeze_period_1) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) }
- let!(:freeze_period_2) { create(:ci_freeze_period, project: project, freeze_start: friday_2330, freeze_end: monday_0800) }
-
- it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 29)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 11, 10, 0)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 7, 59)
-
- it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 8, 1)
- end
-
- # https://gitlab.com/gitlab-org/gitlab/-/issues/370472
- context 'when period overlaps with itself' do
- let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '* * * 8 *', freeze_end: '* * * 10 *') }
-
- it_behaves_like 'within freeze period', Time.utc(2020, 8, 11, 0, 0)
-
- it_behaves_like 'outside freeze period', Time.utc(2020, 10, 11, 0, 0)
- end
-end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 098f8bd4514..18aaab1d1f3 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -356,50 +356,6 @@ RSpec.describe Ci::JobArtifact do
end
end
- describe 'callbacks' do
- describe '#schedule_background_upload' do
- subject { create(:ci_job_artifact, :archive) }
-
- context 'when object storage is disabled' do
- before do
- stub_artifacts_object_storage(enabled: false)
- end
-
- it 'does not schedule the migration' do
- expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
-
- subject
- end
- end
-
- context 'when object storage is enabled' do
- context 'when background upload is enabled' do
- before do
- stub_artifacts_object_storage(background_upload: true)
- end
-
- it 'schedules the model for migration' do
- expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('JobArtifactUploader', described_class.name, :file, kind_of(Numeric))
-
- subject
- end
- end
-
- context 'when background upload is disabled' do
- before do
- stub_artifacts_object_storage(background_upload: false)
- end
-
- it 'schedules the model for migration' do
- expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
-
- subject
- end
- end
- end
- end
- end
-
context 'creating the artifact' do
let(:project) { create(:project) }
let(:artifact) { create(:ci_job_artifact, :archive, project: project) }
diff --git a/spec/models/ci/job_token/allowlist_spec.rb b/spec/models/ci/job_token/allowlist_spec.rb
new file mode 100644
index 00000000000..45083d64393
--- /dev/null
+++ b/spec/models/ci/job_token/allowlist_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integration do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:source_project) { create(:project) }
+
+ let(:allowlist) { described_class.new(source_project, direction: direction) }
+ let(:direction) { :outbound }
+
+ describe '#projects' do
+ subject(:projects) { allowlist.projects }
+
+ context 'when no projects are added to the scope' do
+ [:inbound, :outbound].each do |d|
+ let(:direction) { d }
+
+ it 'returns the project defining the scope' do
+ expect(projects).to contain_exactly(source_project)
+ end
+ end
+ end
+
+ context 'when projects are added to the scope' do
+ include_context 'with scoped projects'
+
+ where(:direction, :additional_project) do
+ :outbound | ref(:outbound_scoped_project)
+ :inbound | ref(:inbound_scoped_project)
+ end
+
+ with_them do
+ it 'returns all projects that can be accessed from a given scope' do
+ expect(projects).to contain_exactly(source_project, additional_project)
+ end
+ end
+ end
+ end
+
+ describe '#includes?' do
+ subject { allowlist.includes?(includes_project) }
+
+ context 'without scoped projects' do
+ let(:unscoped_project) { build(:project) }
+
+ where(:includes_project, :direction, :result) do
+ ref(:source_project) | :outbound | false
+ ref(:source_project) | :inbound | false
+ ref(:unscoped_project) | :outbound | false
+ ref(:unscoped_project) | :inbound | false
+ end
+
+ with_them do
+ it { is_expected.to be result }
+ end
+ end
+
+ context 'with scoped projects' do
+ include_context 'with scoped projects'
+
+ where(:includes_project, :direction, :result) do
+ ref(:source_project) | :outbound | false
+ ref(:source_project) | :inbound | false
+ ref(:inbound_scoped_project) | :outbound | false
+ ref(:inbound_scoped_project) | :inbound | true
+ ref(:outbound_scoped_project) | :outbound | true
+ ref(:outbound_scoped_project) | :inbound | false
+ ref(:unscoped_project1) | :outbound | false
+ ref(:unscoped_project1) | :inbound | false
+ ref(:unscoped_project2) | :outbound | false
+ ref(:unscoped_project2) | :inbound | false
+ end
+
+ with_them do
+ it { is_expected.to be result }
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/job_token/project_scope_link_spec.rb b/spec/models/ci/job_token/project_scope_link_spec.rb
index 92ed86b55b2..91491733c44 100644
--- a/spec/models/ci/job_token/project_scope_link_spec.rb
+++ b/spec/models/ci/job_token/project_scope_link_spec.rb
@@ -2,14 +2,14 @@
require 'spec_helper'
-RSpec.describe Ci::JobToken::ProjectScopeLink do
+RSpec.describe Ci::JobToken::ProjectScopeLink, feature_category: :continuous_integration do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+
it { is_expected.to belong_to(:source_project) }
it { is_expected.to belong_to(:target_project) }
it { is_expected.to belong_to(:added_by) }
- let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project) }
-
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:user) }
let!(:model) { create(:ci_job_token_project_scope_link, added_by: parent) }
@@ -50,8 +50,8 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do
end
end
- describe '.from_project' do
- subject { described_class.from_project(project) }
+ describe '.with_source' do
+ subject { described_class.with_source(project) }
let!(:source_link) { create(:ci_job_token_project_scope_link, source_project: project) }
let!(:target_link) { create(:ci_job_token_project_scope_link, target_project: project) }
@@ -61,8 +61,8 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do
end
end
- describe '.to_project' do
- subject { described_class.to_project(project) }
+ describe '.with_target' do
+ subject { described_class.with_target(project) }
let!(:source_link) { create(:ci_job_token_project_scope_link, source_project: project) }
let!(:target_link) { create(:ci_job_token_project_scope_link, target_project: project) }
diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb
index 1e3f6d044d2..37c56973506 100644
--- a/spec/models/ci/job_token/scope_spec.rb
+++ b/spec/models/ci/job_token/scope_spec.rb
@@ -2,58 +2,72 @@
require 'spec_helper'
-RSpec.describe Ci::JobToken::Scope do
- let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) }
+RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration do
+ let_it_be(:source_project) { create(:project, ci_outbound_job_token_scope_enabled: true) }
- let(:scope) { described_class.new(project) }
+ let(:scope) { described_class.new(source_project) }
describe '#all_projects' do
subject(:all_projects) { scope.all_projects }
context 'when no projects are added to the scope' do
it 'returns the project defining the scope' do
- expect(all_projects).to contain_exactly(project)
+ expect(all_projects).to contain_exactly(source_project)
end
end
- context 'when other projects are added to the scope' do
- let_it_be(:scoped_project) { create(:project) }
- let_it_be(:unscoped_project) { create(:project) }
-
- let!(:link_in_scope) { create(:ci_job_token_project_scope_link, source_project: project, target_project: scoped_project) }
- let!(:link_out_of_scope) { create(:ci_job_token_project_scope_link, target_project: unscoped_project) }
+ context 'when projects are added to the scope' do
+ include_context 'with scoped projects'
it 'returns all projects that can be accessed from a given scope' do
- expect(subject).to contain_exactly(project, scoped_project)
+ expect(subject).to contain_exactly(source_project, outbound_scoped_project)
end
end
end
- describe '#includes?' do
- subject { scope.includes?(target_project) }
+ describe '#allows?' do
+ subject { scope.allows?(includes_project) }
- context 'when param is the project defining the scope' do
- let(:target_project) { project }
+ context 'without scoped projects' do
+ context 'when self referential' do
+ let(:includes_project) { source_project }
- it { is_expected.to be_truthy }
+ it { is_expected.to be_truthy }
+ end
end
- context 'when param is a project in scope' do
- let(:target_link) { create(:ci_job_token_project_scope_link, source_project: project) }
- let(:target_project) { target_link.target_project }
+ context 'with scoped projects' do
+ include_context 'with scoped projects'
- it { is_expected.to be_truthy }
- end
+ context 'when project is in outbound scope' do
+ let(:includes_project) { outbound_scoped_project }
- context 'when param is a project in another scope' do
- let(:scope_link) { create(:ci_job_token_project_scope_link) }
- let(:target_project) { scope_link.target_project }
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when project is in inbound scope' do
+ let(:includes_project) { inbound_scoped_project }
+
+ it { is_expected.to be_falsey }
+ end
- it { is_expected.to be_falsey }
+ context 'when project is linked to a different project' do
+ let(:includes_project) { unscoped_project1 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when project is unlinked to a project' do
+ let(:includes_project) { unscoped_project2 }
+
+ it { is_expected.to be_falsey }
+ end
context 'when project scope setting is disabled' do
+ let(:includes_project) { unscoped_project1 }
+
before do
- project.ci_outbound_job_token_scope_enabled = false
+ source_project.ci_outbound_job_token_scope_enabled = false
end
it 'considers any project to be part of the scope' do
diff --git a/spec/models/ci/job_variable_spec.rb b/spec/models/ci/job_variable_spec.rb
index 4aebd3283f0..0a65708160a 100644
--- a/spec/models/ci/job_variable_spec.rb
+++ b/spec/models/ci/job_variable_spec.rb
@@ -2,11 +2,63 @@
require 'spec_helper'
-RSpec.describe Ci::JobVariable do
- subject { build(:ci_job_variable) }
-
+RSpec.describe Ci::JobVariable, feature_category: :continuous_integration do
it_behaves_like "CI variable"
- it { is_expected.to belong_to(:job) }
- it { is_expected.to validate_uniqueness_of(:key).scoped_to(:job_id) }
+ describe 'associations' do
+ let!(:job_variable) { create(:ci_job_variable) }
+
+ it { is_expected.to belong_to(:job) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:job_id) }
+ end
+
+ describe 'partitioning' do
+ let(:job_variable) { build(:ci_job_variable, job: ci_build) }
+
+ context 'with build' do
+ let(:ci_build) { build(:ci_build, partition_id: ci_testing_partition_id) }
+
+ it 'copies the partition_id from build' do
+ expect { job_variable.valid? }.to change { job_variable.partition_id }.to(ci_testing_partition_id)
+ end
+
+ context 'when it is already set' do
+ let(:job_variable) { build(:ci_job_variable, partition_id: 125) }
+
+ it 'does not change the partition_id value' do
+ expect { job_variable.valid? }.not_to change { job_variable.partition_id }
+ end
+ end
+ end
+
+ context 'without build' do
+ subject(:job_variable) { build(:ci_job_variable, job: nil, partition_id: 125) }
+
+ it { is_expected.to validate_presence_of(:partition_id) }
+
+ it 'does not change the partition_id value' do
+ expect { job_variable.valid? }.not_to change { job_variable.partition_id }
+ end
+ end
+
+ context 'when using bulk_insert', :ci_partitionable do
+ include Ci::PartitioningHelpers
+
+ let(:new_pipeline) { create(:ci_pipeline) }
+ let(:ci_build) { create(:ci_build, pipeline: new_pipeline) }
+ let(:job_variable_2) { build(:ci_job_variable, job: ci_build) }
+
+ before do
+ stub_current_partition_id
+ end
+
+ it 'creates job variables successfully', :aggregate_failures do
+ described_class.bulk_insert!([job_variable, job_variable_2])
+
+ expect(described_class.count).to eq(2)
+ expect(described_class.first.partition_id).to eq(ci_testing_partition_id)
+ expect(described_class.last.partition_id).to eq(ci_testing_partition_id)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/pending_build_spec.rb b/spec/models/ci/pending_build_spec.rb
index 4bb43233dbd..331522070df 100644
--- a/spec/models/ci/pending_build_spec.rb
+++ b/spec/models/ci/pending_build_spec.rb
@@ -196,6 +196,28 @@ RSpec.describe Ci::PendingBuild do
end
end
+ describe 'partitioning', :ci_partitionable do
+ include Ci::PartitioningHelpers
+
+ before do
+ stub_current_partition_id
+ end
+
+ let(:new_pipeline ) { create(:ci_pipeline, project: pipeline.project) }
+ let(:new_build) { create(:ci_build, pipeline: new_pipeline) }
+
+ it 'assigns the same partition id as the one that build has', :aggregate_failures do
+ expect(new_build.partition_id).to eq ci_testing_partition_id
+ expect(new_build.partition_id).not_to eq pipeline.partition_id
+
+ described_class.upsert_from_build!(build)
+ described_class.upsert_from_build!(new_build)
+
+ expect(build.reload.queuing_entry.partition_id).to eq pipeline.partition_id
+ expect(new_build.reload.queuing_entry.partition_id).to eq ci_testing_partition_id
+ end
+ end
+
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:namespace) }
let!(:model) { create(:ci_pending_build, namespace: parent) }
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index b28b61e2b39..9b70f7c2839 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineSchedule do
+RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration do
let_it_be_with_reload(:project) { create_default(:project) }
subject { build(:ci_pipeline_schedule) }
@@ -41,6 +41,12 @@ RSpec.describe Ci::PipelineSchedule do
expect(pipeline_schedule).not_to be_valid
end
+ it 'does not allow empty variable key' do
+ pipeline_schedule = build(:ci_pipeline_schedule, variables_attributes: [{ secret_value: 'test_value' }])
+
+ expect(pipeline_schedule).not_to be_valid
+ end
+
context 'when active is false' do
it 'does not allow nullified ref' do
pipeline_schedule = build(:ci_pipeline_schedule, :inactive, ref: nil)
@@ -110,48 +116,18 @@ RSpec.describe Ci::PipelineSchedule do
end
describe '#set_next_run_at' do
- using RSpec::Parameterized::TableSyntax
-
- where(:worker_cron, :schedule_cron, :plan_limit, :now, :result) do
- '0 1 2 3 *' | '0 1 * * *' | nil | Time.zone.local(2021, 3, 2, 1, 0) | Time.zone.local(2022, 3, 2, 1, 0)
- '0 1 2 3 *' | '0 1 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | Time.zone.local(2021, 3, 2, 1, 0) | Time.zone.local(2022, 3, 2, 1, 0)
- '*/5 * * * *' | '*/1 * * * *' | nil | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 5)
- '*/5 * * * *' | '*/1 * * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0)
- '*/5 * * * *' | '*/1 * * * *' | (1.day.in_minutes / 10).to_i | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10)
- '*/5 * * * *' | '*/1 * * * *' | 200 | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10)
- '*/5 * * * *' | '0 * * * *' | nil | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5)
- '*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 10).to_i | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0)
- '*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0)
- '*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 2.hours.in_minutes).to_i | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5)
- '*/5 * * * *' | '0 1 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 28, 1, 0)
- '*/5 * * * *' | '0 1 * * *' | (1.day.in_minutes / 10).to_i | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 28, 1, 0)
- '*/5 * * * *' | '0 1 * * *' | (1.day.in_minutes / 8).to_i | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 28, 1, 0)
- '*/5 * * * *' | '0 1 1 * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | Time.zone.local(2021, 5, 1, 1, 0) | Time.zone.local(2021, 6, 1, 1, 0)
- '*/9 * * * *' | '0 1 1 * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | Time.zone.local(2021, 5, 1, 1, 9) | Time.zone.local(2021, 6, 1, 1, 0)
- '*/5 * * * *' | '59 14 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | Time.zone.local(2021, 5, 1, 15, 0) | Time.zone.local(2021, 5, 2, 15, 0)
- '*/5 * * * *' | '45 21 1 2 *' | (1.day.in_minutes / 5).to_i | Time.zone.local(2021, 2, 1, 21, 45) | Time.zone.local(2022, 2, 1, 21, 45)
- end
+ let(:now) { Time.zone.local(2021, 3, 2, 1, 0) }
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: "0 1 * * *") }
- with_them do
- let(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: schedule_cron) }
+ it 'calls fallback method next_run_at if there is no plan limit' do
+ allow(Settings).to receive(:cron_jobs).and_return({ 'pipeline_schedule_worker' => { 'cron' => "0 1 2 3 *" } })
- before do
- allow(Settings).to receive(:cron_jobs) do
- { 'pipeline_schedule_worker' => { 'cron' => worker_cron } }
- end
+ travel_to(now) do
+ expect(pipeline_schedule).to receive(:calculate_next_run_at).and_call_original
- create(:plan_limits, :default_plan, ci_daily_pipeline_schedule_triggers: plan_limit) if plan_limit
+ pipeline_schedule.set_next_run_at
- # Setting this here to override initial save with the current time
- pipeline_schedule.next_run_at = now
- end
-
- it 'updates next_run_at' do
- travel_to(now) do
- pipeline_schedule.set_next_run_at
-
- expect(pipeline_schedule.next_run_at).to eq(result)
- end
+ expect(pipeline_schedule.next_run_at).to eq(Time.zone.local(2022, 3, 2, 1, 0))
end
end
@@ -288,6 +264,17 @@ RSpec.describe Ci::PipelineSchedule do
end
end
+ describe '#worker_cron' do
+ before do
+ allow(Settings).to receive(:cron_jobs)
+ .and_return({ pipeline_schedule_worker: { cron: "* 1 2 3 4" } }.with_indifferent_access)
+ end
+
+ it "returns cron expression set in Settings" do
+ expect(subject.worker_cron_expression).to eq("* 1 2 3 4")
+ end
+ end
+
context 'loose foreign key on ci_pipeline_schedules.project_id' do
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:project) }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 2c945898e61..b72693d9994 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -219,6 +219,29 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
+ describe '.for_name' do
+ subject { described_class.for_name(name) }
+
+ let_it_be(:pipeline1) { create(:ci_pipeline, name: 'Build pipeline') }
+ let_it_be(:pipeline2) { create(:ci_pipeline, name: 'Chatops pipeline') }
+
+ context 'when name exists' do
+ let(:name) { 'build Pipeline' }
+
+ it 'performs case insensitive compare' do
+ is_expected.to contain_exactly(pipeline1)
+ end
+ end
+
+ context 'when name does not exist' do
+ let(:name) { 'absent-name' }
+
+ it 'returns empty' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '.created_after' do
let_it_be(:old_pipeline) { create(:ci_pipeline, created_at: 1.week.ago) }
let_it_be(:pipeline) { create(:ci_pipeline) }
@@ -5287,6 +5310,20 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
end
+
+ context 'when the current user is not the bridge user' do
+ let(:current_user) { create(:user) }
+
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ it 'changes bridge user to current user' do
+ expect { reset_bridge }
+ .to change { bridge.reload.user }
+ .from(owner).to(current_user)
+ end
+ end
end
context 'when the user does not have permissions for the processable' do
@@ -5305,6 +5342,15 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
.and not_change { bridge_dependant_dag_job.reload.status }
end
end
+
+ context 'when the current user is not the bridge user' do
+ let(:current_user) { create(:user) }
+
+ it 'does not change bridge user' do
+ expect { reset_bridge }
+ .to not_change { bridge.reload.user }
+ end
+ end
end
end
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index e62e5f84a6d..07fac4ee2f7 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -73,6 +73,7 @@ RSpec.describe Ci::Processable do
job_artifacts_network_referee job_artifacts_dotenv
job_artifacts_cobertura needs job_artifacts_accessibility
job_artifacts_requirements job_artifacts_coverage_fuzzing
+ job_artifacts_requirements_v2
job_artifacts_api_fuzzing terraform_state_versions job_artifacts_cyclonedx].freeze
end
@@ -423,8 +424,8 @@ RSpec.describe Ci::Processable do
it 'returns all needs attributes' do
is_expected.to contain_exactly(
- { 'artifacts' => true, 'name' => 'test1', 'optional' => false },
- { 'artifacts' => true, 'name' => 'test2', 'optional' => false }
+ { 'artifacts' => true, 'name' => 'test1', 'optional' => false, 'partition_id' => build.partition_id },
+ { 'artifacts' => true, 'name' => 'test2', 'optional' => false, 'partition_id' => build.partition_id }
)
end
end
diff --git a/spec/models/ci/resource_group_spec.rb b/spec/models/ci/resource_group_spec.rb
index e8eccc233db..01acf5194f0 100644
--- a/spec/models/ci/resource_group_spec.rb
+++ b/spec/models/ci/resource_group_spec.rb
@@ -33,7 +33,13 @@ RSpec.describe Ci::ResourceGroup do
end
end
- describe '#assign_resource_to' do
+ describe '#assign_resource_to', :ci_partitionable do
+ include Ci::PartitioningHelpers
+
+ before do
+ stub_current_partition_id
+ end
+
subject { resource_group.assign_resource_to(build) }
let(:build) { create(:ci_build) }
@@ -41,10 +47,12 @@ RSpec.describe Ci::ResourceGroup do
it 'retains resource for the processable' do
expect(resource_group.resources.first.processable).to be_nil
+ expect(resource_group.resources.first.partition_id).to be_nil
is_expected.to eq(true)
expect(resource_group.resources.first.processable).to eq(build)
+ expect(resource_group.resources.first.partition_id).to eq(build.partition_id)
end
context 'when there are no free resources' do
@@ -66,7 +74,13 @@ RSpec.describe Ci::ResourceGroup do
end
end
- describe '#release_resource_from' do
+ describe '#release_resource_from', :ci_partitionable do
+ include Ci::PartitioningHelpers
+
+ before do
+ stub_current_partition_id
+ end
+
subject { resource_group.release_resource_from(build) }
let(:build) { create(:ci_build) }
@@ -79,10 +93,12 @@ RSpec.describe Ci::ResourceGroup do
it 'releases resource from the build' do
expect(resource_group.resources.first.processable).to eq(build)
+ expect(resource_group.resources.first.partition_id).to eq(build.partition_id)
is_expected.to eq(true)
expect(resource_group.resources.first.processable).to be_nil
+ expect(resource_group.resources.first.partition_id).to be_nil
end
end
diff --git a/spec/models/ci/runner_namespace_spec.rb b/spec/models/ci/runner_namespace_spec.rb
index 2d1fe11147c..6cbb151d703 100644
--- a/spec/models/ci/runner_namespace_spec.rb
+++ b/spec/models/ci/runner_namespace_spec.rb
@@ -12,4 +12,27 @@ RSpec.describe Ci::RunnerNamespace do
let!(:parent) { model.namespace }
end
+
+ describe '.for_runner' do
+ subject(:for_runner) { described_class.for_runner(runner_ids) }
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:runners) { create_list(:ci_runner, 3, :group, groups: [group]) }
+
+ context 'with runner ids' do
+ let(:runner_ids) { runners[1..2].map(&:id) }
+
+ it 'returns requested runner namespaces' do
+ is_expected.to eq(runners[1..2].flat_map(&:runner_namespaces))
+ end
+ end
+
+ context 'with runners' do
+ let(:runner_ids) { runners.first }
+
+ it 'returns requested runner namespaces' do
+ is_expected.to eq(runners.first.runner_namespaces)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 13eb7086586..803b766c822 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Runner do
+RSpec.describe Ci::Runner, feature_category: :runner do
include StubGitlabCalls
it_behaves_like 'having unique enum values'
@@ -701,6 +701,30 @@ RSpec.describe Ci::Runner do
it { is_expected.to eq([runner1]) }
end
+ describe '.with_running_builds' do
+ subject { described_class.with_running_builds }
+
+ let_it_be(:runner1) { create(:ci_runner) }
+
+ context 'with no builds running' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'with single build running on runner2' do
+ let(:runner2) { create(:ci_runner) }
+ let(:runner3) { create(:ci_runner) }
+
+ before do
+ project = create(:project, :repository)
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, :running, runner: runner2, pipeline: pipeline)
+ create(:ci_build, :running, runner: runner3, pipeline: pipeline)
+ end
+
+ it { is_expected.to contain_exactly(runner2, runner3) }
+ end
+ end
+
describe '#matches_build?' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/models/ci/runner_version_spec.rb b/spec/models/ci/runner_version_spec.rb
index 7a4b2e8f21e..552b271fe85 100644
--- a/spec/models/ci/runner_version_spec.rb
+++ b/spec/models/ci/runner_version_spec.rb
@@ -2,16 +2,16 @@
require 'spec_helper'
-RSpec.describe Ci::RunnerVersion do
- it_behaves_like 'having unique enum values'
+RSpec.describe Ci::RunnerVersion, feature_category: :runner_fleet do
+ let_it_be(:runner_version_recommended) do
+ create(:ci_runner_version, version: 'abc234', status: :recommended)
+ end
let_it_be(:runner_version_not_available) do
create(:ci_runner_version, version: 'abc123', status: :not_available)
end
- let_it_be(:runner_version_recommended) do
- create(:ci_runner_version, version: 'abc234', status: :recommended)
- end
+ it_behaves_like 'having unique enum values'
describe '.not_available' do
subject { described_class.not_available }
@@ -28,11 +28,9 @@ RSpec.describe Ci::RunnerVersion do
end
it 'contains any valid or unprocessed runner version that is not already recommended' do
- is_expected.to match_array([
- runner_version_nil,
- runner_version_not_available,
- runner_version_available
- ])
+ is_expected.to match_array(
+ [runner_version_nil, runner_version_not_available, runner_version_available]
+ )
end
end
diff --git a/spec/models/ci/running_build_spec.rb b/spec/models/ci/running_build_spec.rb
index d2f74494308..1a5ea044ba3 100644
--- a/spec/models/ci/running_build_spec.rb
+++ b/spec/models/ci/running_build_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::RunningBuild do
+RSpec.describe Ci::RunningBuild, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
@@ -50,6 +50,28 @@ RSpec.describe Ci::RunningBuild do
end
end
+ describe 'partitioning', :ci_partitionable do
+ include Ci::PartitioningHelpers
+
+ before do
+ stub_current_partition_id
+ end
+
+ let(:new_pipeline ) { create(:ci_pipeline, project: pipeline.project) }
+ let(:new_build) { create(:ci_build, :running, pipeline: new_pipeline, runner: runner) }
+
+ it 'assigns the same partition id as the one that build has', :aggregate_failures do
+ expect(new_build.partition_id).to eq ci_testing_partition_id
+ expect(new_build.partition_id).not_to eq pipeline.partition_id
+
+ described_class.upsert_shared_runner_build!(build)
+ described_class.upsert_shared_runner_build!(new_build)
+
+ expect(build.reload.runtime_metadata.partition_id).to eq pipeline.partition_id
+ expect(new_build.reload.runtime_metadata.partition_id).to eq ci_testing_partition_id
+ end
+ end
+
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:project) }
let!(:model) { create(:ci_running_build, project: parent) }
diff --git a/spec/models/ci/secure_file_spec.rb b/spec/models/ci/secure_file_spec.rb
index 4413bd8e98b..87077fe2db1 100644
--- a/spec/models/ci/secure_file_spec.rb
+++ b/spec/models/ci/secure_file_spec.rb
@@ -138,17 +138,48 @@ RSpec.describe Ci::SecureFile do
end
describe '#update_metadata!' do
- it 'assigns the expected metadata when a parsable file is supplied' do
+ it 'assigns the expected metadata when a parsable .cer file is supplied' do
file = create(:ci_secure_file, name: 'file1.cer',
file: CarrierWaveStringFile.new(fixture_file('ci_secure_files/sample.cer')))
file.update_metadata!
+ file.reload
+
expect(file.expires_at).to eq(DateTime.parse('2022-04-26 19:20:40'))
expect(file.metadata['id']).to eq('33669367788748363528491290218354043267')
expect(file.metadata['issuer']['CN']).to eq('Apple Worldwide Developer Relations Certification Authority')
expect(file.metadata['subject']['OU']).to eq('N7SYAN8PX8')
end
+ it 'assigns the expected metadata when a parsable .p12 file is supplied' do
+ file = create(:ci_secure_file, name: 'file1.p12',
+ file: CarrierWaveStringFile.new(fixture_file('ci_secure_files/sample.p12')))
+ file.update_metadata!
+
+ file.reload
+
+ expect(file.expires_at).to eq(DateTime.parse('2022-09-21 14:56:00'))
+ expect(file.metadata['id']).to eq('75949910542696343243264405377658443914')
+ expect(file.metadata['issuer']['CN']).to eq('Apple Worldwide Developer Relations Certification Authority')
+ expect(file.metadata['subject']['OU']).to eq('N7SYAN8PX8')
+ end
+
+ it 'assigns the expected metadata when a parsable .mobileprovision file is supplied' do
+ file = create(:ci_secure_file, name: 'file1.mobileprovision',
+ file: CarrierWaveStringFile.new(
+ fixture_file('ci_secure_files/sample.mobileprovision')
+ ))
+ file.update_metadata!
+
+ file.reload
+
+ expect(file.expires_at).to eq(DateTime.parse('2023-08-01 23:15:13'))
+ expect(file.metadata['id']).to eq('6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf')
+ expect(file.metadata['platforms'].first).to eq('iOS')
+ expect(file.metadata['app_name']).to eq('iOS Demo')
+ expect(file.metadata['app_id']).to eq('match Development com.gitlab.ios-demo')
+ end
+
it 'logs an error when something goes wrong with the file parsing' do
corrupt_file = create(:ci_secure_file, name: 'file1.cer', file: CarrierWaveStringFile.new('11111111'))
message = 'Validation failed: Metadata must be a valid json schema - not enough data.'
diff --git a/spec/models/ci/sources/pipeline_spec.rb b/spec/models/ci/sources/pipeline_spec.rb
index fdc1c111c40..707872d0a15 100644
--- a/spec/models/ci/sources/pipeline_spec.rb
+++ b/spec/models/ci/sources/pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Sources::Pipeline do
+RSpec.describe Ci::Sources::Pipeline, feature_category: :continuous_integration do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:pipeline) }
@@ -31,4 +31,20 @@ RSpec.describe Ci::Sources::Pipeline do
let!(:model) { create(:ci_sources_pipeline, project: parent) }
end
end
+
+ describe 'partitioning', :ci_partitioning do
+ include Ci::PartitioningHelpers
+
+ let(:new_pipeline) { create(:ci_pipeline) }
+ let(:source_pipeline) { create(:ci_sources_pipeline, pipeline: new_pipeline) }
+
+ before do
+ stub_current_partition_id
+ end
+
+ it 'assigns partition_id and source_partition_id from pipeline and source_job', :aggregate_failures do
+ expect(source_pipeline.partition_id).to eq(ci_testing_partition_id)
+ expect(source_pipeline.source_partition_id).to eq(ci_testing_partition_id)
+ end
+ end
end
diff --git a/spec/models/ci/unit_test_failure_spec.rb b/spec/models/ci/unit_test_failure_spec.rb
index f9b8c66b603..7ffd103bc7d 100644
--- a/spec/models/ci/unit_test_failure_spec.rb
+++ b/spec/models/ci/unit_test_failure_spec.rb
@@ -70,4 +70,46 @@ RSpec.describe Ci::UnitTestFailure do
end
end
end
+
+ describe 'partitioning', :ci_partitionable do
+ let(:project) { FactoryBot.build(:project) }
+ let(:unit_test) { FactoryBot.build(:ci_unit_test, project: project) }
+
+ context 'with build' do
+ let(:build) { FactoryBot.build(:ci_build, partition_id: ci_testing_partition_id) }
+ let(:unit_test_failure) do
+ FactoryBot.build(:ci_unit_test_failure, build: build, unit_test: unit_test, failed_at: 1.day.ago)
+ end
+
+ it 'copies the partition_id from build' do
+ expect { unit_test_failure.valid? }.to change { unit_test_failure.partition_id }.to(ci_testing_partition_id)
+ end
+
+ context 'when it is already set' do
+ let(:unit_test_failure) do
+ FactoryBot.build(
+ :ci_unit_test_failure,
+ build: build,
+ unit_test: unit_test,
+ failed_at: 1.day.ago,
+ partition_id: 125
+ )
+ end
+
+ it 'does not change the partition_id value' do
+ expect { unit_test_failure.valid? }.not_to change { unit_test_failure.partition_id }
+ end
+ end
+ end
+
+ context 'without build' do
+ subject(:unit_test_failure) { FactoryBot.build(:ci_unit_test_failure, build: nil, partition_id: 125) }
+
+ it { is_expected.to validate_presence_of(:partition_id) }
+
+ it 'does not change the partition_id value' do
+ expect { unit_test_failure.valid? }.not_to change { unit_test_failure.partition_id }
+ end
+ end
+ end
end
diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb
index efa2a3eb09b..74723e3abd8 100644
--- a/spec/models/clusters/agent_token_spec.rb
+++ b/spec/models/clusters/agent_token_spec.rb
@@ -49,4 +49,12 @@ RSpec.describe Clusters::AgentToken do
expect(agent_token.token.length).to be >= 50
end
end
+
+ describe '#to_ability_name' do
+ it 'is :cluster' do
+ agent_token = build(:cluster_agent_token)
+
+ expect(agent_token.to_ability_name).to eq :cluster
+ end
+ end
end
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index e5caa11452e..2be59e5f515 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -5,6 +5,11 @@ require 'spec_helper'
RSpec.describe Clusters::Applications::Ingress do
let(:ingress) { create(:clusters_applications_ingress) }
+ before do
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
+ end
+
it_behaves_like 'having unique enum values'
include_examples 'cluster application core specs', :clusters_applications_ingress
@@ -13,11 +18,6 @@ RSpec.describe Clusters::Applications::Ingress do
include_examples 'cluster application helm specs', :clusters_applications_ingress
include_examples 'cluster application initial status specs'
- before do
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
- end
-
describe 'default values' do
it { expect(subject.ingress_type).to eq("nginx") }
it { expect(subject.version).to eq(described_class::VERSION) }
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index 3914450339a..f6b13f4a93f 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -5,18 +5,18 @@ require 'spec_helper'
RSpec.describe Clusters::Applications::Knative do
let(:knative) { create(:clusters_applications_knative) }
- include_examples 'cluster application core specs', :clusters_applications_knative
- include_examples 'cluster application status specs', :clusters_applications_knative
- include_examples 'cluster application helm specs', :clusters_applications_knative
- include_examples 'cluster application version specs', :clusters_applications_knative
- include_examples 'cluster application initial status specs'
-
before do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
allow(ClusterConfigureIstioWorker).to receive(:perform_async)
end
+ include_examples 'cluster application core specs', :clusters_applications_knative
+ include_examples 'cluster application status specs', :clusters_applications_knative
+ include_examples 'cluster application helm specs', :clusters_applications_knative
+ include_examples 'cluster application version specs', :clusters_applications_knative
+ include_examples 'cluster application initial status specs'
+
describe 'associations' do
it { is_expected.to have_one(:serverless_domain_cluster).class_name('::Serverless::DomainCluster').with_foreign_key('clusters_applications_knative_id').inverse_of(:knative) }
end
diff --git a/spec/models/commit_signatures/gpg_signature_spec.rb b/spec/models/commit_signatures/gpg_signature_spec.rb
index 1ffaaeba396..75cc5d448df 100644
--- a/spec/models/commit_signatures/gpg_signature_spec.rb
+++ b/spec/models/commit_signatures/gpg_signature_spec.rb
@@ -23,6 +23,7 @@ RSpec.describe CommitSignatures::GpgSignature do
it_behaves_like 'having unique enum values'
it_behaves_like 'commit signature'
+ it_behaves_like 'signature with type checking', :gpg
describe 'associations' do
it { is_expected.to belong_to(:gpg_key) }
@@ -86,9 +87,9 @@ RSpec.describe CommitSignatures::GpgSignature do
end
end
- describe '#user' do
+ describe '#signed_by_user' do
it 'retrieves the gpg_key user' do
- expect(signature.user).to eq(gpg_key.user)
+ expect(signature.signed_by_user).to eq(gpg_key.user)
end
end
end
diff --git a/spec/models/commit_signatures/ssh_signature_spec.rb b/spec/models/commit_signatures/ssh_signature_spec.rb
index 08530bf6964..629d9c5ec53 100644
--- a/spec/models/commit_signatures/ssh_signature_spec.rb
+++ b/spec/models/commit_signatures/ssh_signature_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe CommitSignatures::SshSignature do
it_behaves_like 'having unique enum values'
it_behaves_like 'commit signature'
+ it_behaves_like 'signature with type checking', :ssh
describe 'associations' do
it { is_expected.to belong_to(:key).optional }
@@ -37,4 +38,10 @@ RSpec.describe CommitSignatures::SshSignature do
).to contain_exactly(signature, another_signature)
end
end
+
+ describe '#signed_by_user' do
+ it 'returns the user associated with the SSH key' do
+ expect(signature.signed_by_user).to eq(ssh_key.user)
+ end
+ end
end
diff --git a/spec/models/commit_signatures/x509_commit_signature_spec.rb b/spec/models/commit_signatures/x509_commit_signature_spec.rb
index b971fd078e2..cceb96ec70d 100644
--- a/spec/models/commit_signatures/x509_commit_signature_spec.rb
+++ b/spec/models/commit_signatures/x509_commit_signature_spec.rb
@@ -23,6 +23,7 @@ RSpec.describe CommitSignatures::X509CommitSignature do
it_behaves_like 'having unique enum values'
it_behaves_like 'commit signature'
+ it_behaves_like 'signature with type checking', :x509
describe 'validation' do
it { is_expected.to validate_presence_of(:x509_certificate_id) }
@@ -37,12 +38,12 @@ RSpec.describe CommitSignatures::X509CommitSignature do
let!(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
it 'returns user' do
- expect(described_class.safe_create!(attributes).user).to eq(user)
+ expect(described_class.safe_create!(attributes).signed_by_user).to eq(user)
end
end
it 'if email is not assigned to a user, return nil' do
- expect(described_class.safe_create!(attributes).user).to be_nil
+ expect(described_class.safe_create!(attributes).signed_by_user).to be_nil
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index bab6247d4f9..4b5aabe745b 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -828,12 +828,14 @@ eos
describe 'signed commits' do
let(:gpg_signed_commit) { project.commit_by(oid: '0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
let(:x509_signed_commit) { project.commit_by(oid: '189a6c924013fc3fe40d6f1ec1dc20214183bc97') }
+ let(:ssh_signed_commit) { project.commit_by(oid: '7b5160f9bb23a3d58a0accdbe89da13b96b1ece9') }
let(:unsigned_commit) { project.commit_by(oid: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51') }
let!(:commit) { create(:commit, project: project) }
it 'returns signature_type properly' do
expect(gpg_signed_commit.signature_type).to eq(:PGP)
expect(x509_signed_commit.signature_type).to eq(:X509)
+ expect(ssh_signed_commit.signature_type).to eq(:SSH)
expect(unsigned_commit.signature_type).to eq(:NONE)
expect(commit.signature_type).to eq(:NONE)
end
@@ -841,9 +843,24 @@ eos
it 'returns has_signature? properly' do
expect(gpg_signed_commit.has_signature?).to be_truthy
expect(x509_signed_commit.has_signature?).to be_truthy
+ expect(ssh_signed_commit.has_signature?).to be_truthy
expect(unsigned_commit.has_signature?).to be_falsey
expect(commit.has_signature?).to be_falsey
end
+
+ context 'when feature flag "ssh_commit_signatures" is disabled' do
+ before do
+ stub_feature_flags(ssh_commit_signatures: false)
+ end
+
+ it 'reports no signature' do
+ expect(ssh_signed_commit).not_to have_signature
+ end
+
+ it 'does not return signature data' do
+ expect(ssh_signed_commit.signature).to be_nil
+ end
+ end
end
describe '#has_been_reverted?' do
diff --git a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
index 358000ee174..e8d84fe9630 100644
--- a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
+++ b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
@@ -27,6 +27,7 @@ RSpec.describe BatchDestroyDependentAssociations do
let_it_be(:build) { create(:ci_build, project: project) }
let_it_be(:notification_setting) { create(:notification_setting, project: project) }
let_it_be(:note) { create(:note, project: project) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
it 'destroys multiple notes' do
create(:note, project: project)
@@ -51,9 +52,10 @@ RSpec.describe BatchDestroyDependentAssociations do
end
it 'excludes associations' do
- project.destroy_dependent_associations_in_batches(exclude: [:notes])
+ project.destroy_dependent_associations_in_batches(exclude: [:merge_requests])
- expect(Note.count).to eq(1)
+ expect(MergeRequest.count).to eq(1)
+ expect(Note.count).to eq(0)
expect(Ci::Build.count).to eq(1)
expect(User.count).to be > 0
expect(NotificationSetting.count).to eq(User.count)
diff --git a/spec/models/concerns/bulk_insertable_associations_spec.rb b/spec/models/concerns/bulk_insertable_associations_spec.rb
index 9713f1ce9a4..3187dcd8f93 100644
--- a/spec/models/concerns/bulk_insertable_associations_spec.rb
+++ b/spec/models/concerns/bulk_insertable_associations_spec.rb
@@ -3,34 +3,41 @@
require 'spec_helper'
RSpec.describe BulkInsertableAssociations do
- class BulkFoo < ApplicationRecord
- include BulkInsertSafe
+ before do
+ stub_const('BulkFoo', Class.new(ApplicationRecord))
+ stub_const('BulkBar', Class.new(ApplicationRecord))
+ stub_const('SimpleBar', Class.new(ApplicationRecord))
+ stub_const('BulkParent', Class.new(ApplicationRecord))
- self.table_name = '_test_bulk_foos'
+ BulkFoo.class_eval do
+ include BulkInsertSafe
- validates :name, presence: true
- end
+ self.table_name = '_test_bulk_foos'
- class BulkBar < ApplicationRecord
- include BulkInsertSafe
+ validates :name, presence: true
+ end
- self.table_name = '_test_bulk_bars'
- end
+ BulkBar.class_eval do
+ include BulkInsertSafe
- SimpleBar = Class.new(ApplicationRecord) do
- self.table_name = '_test_simple_bars'
- end
+ self.table_name = '_test_bulk_bars'
+ end
+
+ SimpleBar.class_eval do
+ self.table_name = '_test_simple_bars'
+ end
- class BulkParent < ApplicationRecord
- include BulkInsertableAssociations
+ BulkParent.class_eval do
+ include BulkInsertableAssociations
- self.table_name = '_test_bulk_parents'
+ self.table_name = '_test_bulk_parents'
- has_many :bulk_foos, class_name: 'BulkFoo'
- has_many :bulk_hunks, class_name: 'BulkFoo'
- has_many :bulk_bars, class_name: 'BulkBar'
- has_many :simple_bars, class_name: 'SimpleBar' # not `BulkInsertSafe`
- has_one :bulk_foo # not supported
+ has_many :bulk_foos, class_name: 'BulkFoo'
+ has_many :bulk_hunks, class_name: 'BulkFoo'
+ has_many :bulk_bars, class_name: 'BulkBar'
+ has_many :simple_bars, class_name: 'SimpleBar' # not `BulkInsertSafe`
+ has_one :bulk_foo # not supported
+ end
end
before(:all) do
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 19b9a1519eb..f85f636ebe5 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -231,7 +231,7 @@ RSpec.describe CacheMarkdownField, :clean_gitlab_redis_cache do
end
describe '#updated_cached_html_for' do
- let(:thing) { klass.new(description: markdown, description_html: html, cached_markdown_version: cache_version) }
+ let(:thing) { klass.new(project_id: project.id, namespace_id: project.project_namespace_id, description: markdown, description_html: html, cached_markdown_version: cache_version) }
context 'when the markdown cache is outdated' do
before do
@@ -286,7 +286,7 @@ RSpec.describe CacheMarkdownField, :clean_gitlab_redis_cache do
end
shared_examples 'a class with mentionable markdown fields' do
- let(:mentionable) { klass.new(description: markdown, description_html: html, title: markdown, title_html: html, cached_markdown_version: cache_version) }
+ let(:mentionable) { klass.new(project_id: project.id, namespace_id: project.project_namespace_id, description: markdown, description_html: html, title: markdown, title_html: html, cached_markdown_version: cache_version) }
context 'when klass is a Mentionable', :aggregate_failures do
before do
@@ -336,7 +336,7 @@ RSpec.describe CacheMarkdownField, :clean_gitlab_redis_cache do
end
context 'when the markdown field also a mentionable attribute' do
- let(:thing) { klass.new(description: markdown, description_html: html, cached_markdown_version: cache_version) }
+ let(:thing) { klass.new(project_id: project.id, namespace_id: project.project_namespace_id, description: markdown, description_html: html, cached_markdown_version: cache_version) }
it 'calls #store_mentions!' do
expect(thing).to receive(:mentionable_attributes_changed?).and_return(true)
@@ -349,7 +349,7 @@ RSpec.describe CacheMarkdownField, :clean_gitlab_redis_cache do
end
context 'when the markdown field is not mentionable attribute' do
- let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
+ let(:thing) { klass.new(project_id: project.id, namespace_id: project.project_namespace_id, title: markdown, title_html: html, cached_markdown_version: cache_version) }
it 'does not call #store_mentions!' do
expect(thing).not_to receive(:store_mentions!)
@@ -363,7 +363,7 @@ RSpec.describe CacheMarkdownField, :clean_gitlab_redis_cache do
end
context 'when the markdown field does not exist' do
- let(:thing) { klass.new(cached_markdown_version: cache_version) }
+ let(:thing) { klass.new(project_id: project.id, namespace_id: project.project_namespace_id, cached_markdown_version: cache_version) }
it 'does not call #store_mentions!' do
expect(thing).not_to receive(:store_mentions!)
@@ -376,13 +376,15 @@ RSpec.describe CacheMarkdownField, :clean_gitlab_redis_cache do
end
context 'for Active record classes' do
+ let_it_be(:project) { create(:project) }
+
let(:klass) { ar_class }
it_behaves_like 'a class with cached markdown fields'
it_behaves_like 'a class with mentionable markdown fields'
describe '#attribute_invalidated?' do
- let(:thing) { klass.create!(description: markdown, description_html: html, cached_markdown_version: cache_version) }
+ let(:thing) { klass.create!(project_id: project.id, namespace_id: project.project_namespace_id, description: markdown, description_html: html, cached_markdown_version: cache_version) }
it 'returns true when cached_markdown_version is different' do
thing.cached_markdown_version += 1
@@ -425,7 +427,7 @@ RSpec.describe CacheMarkdownField, :clean_gitlab_redis_cache do
let(:thing) do
# This forces the record to have outdated HTML. We can't use `create` because the `before_create` hook
# would re-render the HTML to the latest version
- klass.create!.tap do |thing|
+ klass.create!(project_id: project.id, namespace_id: project.project_namespace_id).tap do |thing|
thing.update_columns(description: markdown, description_html: old_html, cached_markdown_version: old_version)
end
end
@@ -441,6 +443,8 @@ RSpec.describe CacheMarkdownField, :clean_gitlab_redis_cache do
end
context 'for other classes' do
+ let_it_be(:project) { create(:project) }
+
let(:klass) { other_class }
it_behaves_like 'a class with cached markdown fields'
diff --git a/spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb b/spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb
new file mode 100644
index 00000000000..bb25d7d1665
--- /dev/null
+++ b/spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Partitionable::PartitionedFilter, :aggregate_failures, feature_category: :continuous_integration do
+ before do
+ create_tables(<<~SQL)
+ CREATE TABLE _test_ci_jobs_metadata (
+ id serial NOT NULL,
+ partition_id int NOT NULL DEFAULT 10,
+ name text,
+ PRIMARY KEY (id, partition_id)
+ ) PARTITION BY LIST(partition_id);
+
+ CREATE TABLE _test_ci_jobs_metadata_1
+ PARTITION OF _test_ci_jobs_metadata
+ FOR VALUES IN (10);
+ SQL
+ end
+
+ let(:model) do
+ Class.new(Ci::ApplicationRecord) do
+ include Ci::Partitionable::PartitionedFilter
+
+ self.primary_key = :id
+ self.table_name = :_test_ci_jobs_metadata
+
+ def self.name
+ 'TestCiJobMetadata'
+ end
+ end
+ end
+
+ let!(:record) { model.create! }
+
+ let(:where_filter) do
+ /WHERE "_test_ci_jobs_metadata"."id" = #{record.id} AND "_test_ci_jobs_metadata"."partition_id" = 10/
+ end
+
+ describe '#save' do
+ it 'uses id and partition_id' do
+ record.name = 'test'
+ recorder = ActiveRecord::QueryRecorder.new { record.save! }
+
+ expect(recorder.log).to include(where_filter)
+ expect(record.name).to eq('test')
+ end
+ end
+
+ describe '#update' do
+ it 'uses id and partition_id' do
+ recorder = ActiveRecord::QueryRecorder.new { record.update!(name: 'test') }
+
+ expect(recorder.log).to include(where_filter)
+ expect(record.name).to eq('test')
+ end
+ end
+
+ describe '#delete' do
+ it 'uses id and partition_id' do
+ recorder = ActiveRecord::QueryRecorder.new { record.delete }
+
+ expect(recorder.log).to include(where_filter)
+ expect(model.count).to be_zero
+ end
+ end
+
+ describe '#destroy' do
+ it 'uses id and partition_id' do
+ recorder = ActiveRecord::QueryRecorder.new { record.destroy! }
+
+ expect(recorder.log).to include(where_filter)
+ expect(model.count).to be_zero
+ end
+ end
+
+ def create_tables(table_sql)
+ Ci::ApplicationRecord.connection.execute(table_sql)
+ end
+end
diff --git a/spec/models/concerns/ci/partitionable_spec.rb b/spec/models/concerns/ci/partitionable_spec.rb
index f3d33c971c7..430ef57d493 100644
--- a/spec/models/concerns/ci/partitionable_spec.rb
+++ b/spec/models/concerns/ci/partitionable_spec.rb
@@ -40,4 +40,28 @@ RSpec.describe Ci::Partitionable do
it { expect(ci_model.ancestors).to include(described_class::Switch) }
end
+
+ context 'with partitioned options' do
+ before do
+ stub_const("#{described_class}::Testing::PARTITIONABLE_MODELS", [ci_model.name])
+
+ ci_model.include(described_class)
+ ci_model.partitionable scope: ->(r) { 1 }, partitioned: partitioned
+ end
+
+ context 'when partitioned is true' do
+ let(:partitioned) { true }
+
+ it { expect(ci_model.ancestors).to include(described_class::PartitionedFilter) }
+ it { expect(ci_model).to be_partitioned }
+ end
+
+ context 'when partitioned is false' do
+ let(:partitioned) { false }
+
+ it { expect(ci_model.ancestors).not_to include(described_class::PartitionedFilter) }
+
+ it { expect(ci_model).not_to be_partitioned }
+ end
+ end
end
diff --git a/spec/models/concerns/commit_signature_spec.rb b/spec/models/concerns/commit_signature_spec.rb
new file mode 100644
index 00000000000..4bba5a6ee41
--- /dev/null
+++ b/spec/models/concerns/commit_signature_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CommitSignature do
+ describe '#signed_by_user' do
+ context 'when class does not define the signed_by_user method' do
+ subject(:implementation) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'ssh_signatures'
+ end.include(described_class).new
+ end
+
+ it 'raises a NoMethodError with custom message' do
+ expect do
+ implementation.signed_by_user
+ end.to raise_error(NoMethodError, 'must implement `signed_by_user` method')
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/counter_attribute_spec.rb b/spec/models/concerns/counter_attribute_spec.rb
index 66ccd4559e5..1dd9b78d642 100644
--- a/spec/models/concerns/counter_attribute_spec.rb
+++ b/spec/models/concerns/counter_attribute_spec.rb
@@ -8,85 +8,70 @@ RSpec.describe CounterAttribute, :counter_attribute, :clean_gitlab_redis_shared_
let(:project_statistics) { create(:project_statistics) }
let(:model) { CounterAttributeModel.find(project_statistics.id) }
- it_behaves_like CounterAttribute, [:build_artifacts_size, :commit_count] do
+ it_behaves_like CounterAttribute, [:build_artifacts_size, :commit_count, :packages_size] do
let(:model) { CounterAttributeModel.find(project_statistics.id) }
end
- describe 'after_flush callbacks' do
- let(:attribute) { model.class.counter_attributes.first }
-
- subject { model.flush_increments_to_database!(attribute) }
+ describe '#counter_attribute_enabled?' do
+ it 'is true when counter attribute is defined' do
+ expect(project_statistics.counter_attribute_enabled?(:build_artifacts_size))
+ .to be_truthy
+ end
- it 'has registered callbacks' do # defined in :counter_attribute RSpec tag
- expect(model.class.after_flush_callbacks.size).to eq(1)
+ it 'is false when counter attribute is not defined' do
+ expect(model.counter_attribute_enabled?(:nope)).to be_falsey
end
- context 'when there are increments to flush' do
- before do
- model.delayed_increment_counter(attribute, 10)
- end
+ context 'with a conditional counter attribute' do
+ [true, false].each do |enabled|
+ context "where the condition evaluates to #{enabled}" do
+ subject { model.counter_attribute_enabled?(:packages_size) }
- it 'executes the callbacks' do
- subject
+ before do
+ model.allow_package_size_counter = enabled
+ end
- expect(model.flushed).to be_truthy
+ it { is_expected.to eq(enabled) }
+ end
end
end
+ end
- context 'when there are no increments to flush' do
- it 'does not execute the callbacks' do
- subject
+ describe '#counter' do
+ using RSpec::Parameterized::TableSyntax
- expect(model.flushed).to be_nil
- end
+ it 'returns the counter for the respective attribute' do
+ expect(model.counter(:build_artifacts_size).send(:attribute)).to eq(:build_artifacts_size)
+ expect(model.counter(:commit_count).send(:attribute)).to eq(:commit_count)
end
- end
- describe '.steal_increments' do
- let(:increment_key) { 'counters:Model:123:attribute' }
- let(:flushed_key) { 'counter:Model:123:attribute:flushed' }
-
- subject { model.send(:steal_increments, increment_key, flushed_key) }
-
- where(:increment, :flushed, :result, :flushed_key_present) do
- nil | nil | 0 | false
- nil | 0 | 0 | false
- 0 | 0 | 0 | false
- 1 | 0 | 1 | true
- 1 | nil | 1 | true
- 1 | 1 | 2 | true
- 1 | -2 | -1 | true
- -1 | 1 | 0 | false
+ it 'returns the appropriate counter for the attribute' do
+ expect(model.counter(:build_artifacts_size).class).to eq(Gitlab::Counters::BufferedCounter)
+ expect(model.counter(:packages_size).class).to eq(Gitlab::Counters::BufferedCounter)
+ expect(model.counter(:wiki_size).class).to eq(Gitlab::Counters::LegacyCounter)
end
- with_them do
- before do
- Gitlab::Redis::SharedState.with do |redis|
- redis.set(increment_key, increment) if increment
- redis.set(flushed_key, flushed) if flushed
- end
+ context 'with a conditional counter attribute' do
+ where(:enabled, :expected_counter_class) do
+ [
+ [true, Gitlab::Counters::BufferedCounter],
+ [false, Gitlab::Counters::LegacyCounter]
+ ]
end
- it { is_expected.to eq(result) }
-
- it 'drops the increment key and creates the flushed key if it does not exist' do
- subject
+ with_them do
+ before do
+ model.allow_package_size_counter = enabled
+ end
- Gitlab::Redis::SharedState.with do |redis|
- expect(redis.exists?(increment_key)).to eq(false)
- expect(redis.exists?(flushed_key)).to eq(flushed_key_present)
+ it 'returns the appropriate counter based on the condition' do
+ expect(model.counter(:packages_size).class).to eq(expected_counter_class)
end
end
end
- end
-
- describe '.counter_attribute_enabled?' do
- it 'is true when counter attribute is defined' do
- expect(CounterAttributeModel.counter_attribute_enabled?(:build_artifacts_size)).to be_truthy
- end
- it 'is false when counter attribute is not defined' do
- expect(CounterAttributeModel.counter_attribute_enabled?(:nope)).to be_falsey
+ it 'raises error for unknown attribute' do
+ expect { model.counter(:unknown) }.to raise_error(ArgumentError, 'attribute "unknown" does not exist')
end
end
end
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
index b2ea7b22dea..462b28f99be 100644
--- a/spec/models/concerns/has_user_type_spec.rb
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe User do
specify 'types consistency checks', :aggregate_failures do
expect(described_class::USER_TYPES.keys)
- .to match_array(%w[human ghost alert_bot project_bot support_bot service_user security_bot visual_review_bot migration_bot automation_bot])
+ .to match_array(%w[human ghost alert_bot project_bot support_bot service_user security_bot visual_review_bot migration_bot automation_bot admin_bot])
expect(described_class::USER_TYPES).to include(*described_class::BOT_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::NON_INTERNAL_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::INTERNAL_USER_TYPES)
@@ -37,12 +37,6 @@ RSpec.describe User do
end
end
- describe '.bots_without_project_bot' do
- it 'includes all bots except project_bot' do
- expect(described_class.bots_without_project_bot).to match_array(bots - [project_bot])
- end
- end
-
describe '.non_internal' do
it 'includes all non_internal users' do
expect(described_class.non_internal).to match_array(non_internal)
diff --git a/spec/models/concerns/pg_full_text_searchable_spec.rb b/spec/models/concerns/pg_full_text_searchable_spec.rb
index 98b44a2eec2..87f1dc5a27b 100644
--- a/spec/models/concerns/pg_full_text_searchable_spec.rb
+++ b/spec/models/concerns/pg_full_text_searchable_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe PgFullTextSearchable do
- let(:project) { build(:project) }
+ let(:project) { build(:project, project_namespace: build(:project_namespace)) }
let(:model_class) do
Class.new(ActiveRecord::Base) do
@@ -12,6 +12,7 @@ RSpec.describe PgFullTextSearchable do
self.table_name = 'issues'
belongs_to :project
+ belongs_to :namespace
has_one :search_data, class_name: 'Issues::SearchData'
before_validation -> { self.work_item_type_id = ::WorkItems::Type.default_issue_type.id }
@@ -41,7 +42,7 @@ RSpec.describe PgFullTextSearchable do
end
describe 'after commit hook' do
- let(:model) { model_class.create!(project: project) }
+ let(:model) { model_class.create!(project: project, namespace: project.project_namespace) }
before do
model_class.pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }]
@@ -76,9 +77,9 @@ RSpec.describe PgFullTextSearchable do
end
describe '.pg_full_text_search' do
- let(:english) { model_class.create!(project: project, title: 'title', description: 'something description english') }
- let(:with_accent) { model_class.create!(project: project, title: 'Jürgen', description: 'Ærøskøbing') }
- let(:japanese) { model_class.create!(project: project, title: '日本語 title', description: 'another english description') }
+ let(:english) { model_class.create!(project: project, namespace: project.project_namespace, title: 'title', description: 'something description english') }
+ let(:with_accent) { model_class.create!(project: project, namespace: project.project_namespace, title: 'Jürgen', description: 'Ærøskøbing') }
+ let(:japanese) { model_class.create!(project: project, namespace: project.project_namespace, title: '日本語 title', description: 'another english description') }
before do
model_class.pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }, { name: 'description', weight: 'B' }]
@@ -91,7 +92,7 @@ RSpec.describe PgFullTextSearchable do
end
it 'searches specified columns only' do
- matching_object = model_class.create!(project: project, title: 'english', description: 'some description')
+ 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)
@@ -115,7 +116,7 @@ RSpec.describe PgFullTextSearchable do
end
context 'when search term has a URL' do
- let(:with_url) { model_class.create!(project: project, title: 'issue with url', description: 'sample url,https://gitlab.com/gitlab-org/gitlab') }
+ let(:with_url) { model_class.create!(project: project, namespace: project.project_namespace, title: 'issue with url', description: 'sample url,https://gitlab.com/gitlab-org/gitlab') }
it 'allows searching by full URL, ignoring the scheme' do
with_url.update_search_data!
@@ -127,7 +128,7 @@ RSpec.describe PgFullTextSearchable do
context 'when search term is a path with underscores' do
let(:path) { 'browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb' }
- let(:with_underscore) { model_class.create!(project: project, title: 'issue with path', description: "some #{path} other text") }
+ let(:with_underscore) { model_class.create!(project: project, namespace: project.project_namespace, title: 'issue with path', description: "some #{path} other text") }
it 'allows searching by the path' do
with_underscore.update_search_data!
@@ -137,7 +138,7 @@ RSpec.describe PgFullTextSearchable do
end
context 'when text has numbers preceded by a dash' do
- let(:with_dash) { model_class.create!(project: project, title: 'issue with dash', description: 'ABC-123') }
+ let(:with_dash) { model_class.create!(project: project, namespace: project.project_namespace, title: 'issue with dash', description: 'ABC-123') }
it 'allows searching by numbers only' do
with_dash.update_search_data!
@@ -148,7 +149,7 @@ RSpec.describe PgFullTextSearchable do
end
describe '#update_search_data!' do
- let(:model) { model_class.create!(project: project, title: 'title', description: 'description') }
+ let(:model) { model_class.create!(project: project, namespace: project.project_namespace, title: 'title', description: 'description') }
before do
model_class.pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }, { name: 'description', weight: 'B' }]
@@ -162,7 +163,7 @@ RSpec.describe PgFullTextSearchable do
end
context 'with accented and non-Latin characters' do
- let(:model) { model_class.create!(project: project, title: '日本語', description: 'Jürgen') }
+ let(:model) { model_class.create!(project: project, namespace: project.project_namespace, title: '日本語', description: 'Jürgen') }
it 'transliterates accented characters and removes non-Latin ones' do
model.update_search_data!
@@ -173,7 +174,7 @@ RSpec.describe PgFullTextSearchable do
end
context 'with long words' do
- let(:model) { model_class.create!(project: project, title: 'title ' + 'long/sequence+1' * 4, description: 'description ' + '@user1' * 20) }
+ let(:model) { model_class.create!(project: project, namespace: project.project_namespace, title: 'title ' + 'long/sequence+1' * 4, description: 'description ' + '@user1' * 20) }
it 'strips words that are 50 characters or longer' do
model.update_search_data!
@@ -197,7 +198,7 @@ RSpec.describe PgFullTextSearchable do
context 'with strings that go over tsvector limit', :delete do
let(:long_string) { Array.new(30_000) { SecureRandom.hex }.join(' ') }
- let(:model) { model_class.create!(project: project, title: 'title', description: long_string) }
+ let(:model) { model_class.create!(project: project, namespace: project.project_namespace, title: 'title', description: long_string) }
it 'does not raise an exception' do
expect(Gitlab::AppJsonLogger).to receive(:error).with(
@@ -218,6 +219,7 @@ RSpec.describe PgFullTextSearchable do
self.table_name = 'issues'
belongs_to :project
+ belongs_to :namespace
has_one :search_data, class_name: 'Issues::SearchData'
before_validation -> { self.work_item_type_id = ::WorkItems::Type.default_issue_type.id }
diff --git a/spec/models/concerns/schedulable_spec.rb b/spec/models/concerns/schedulable_spec.rb
index b98dcf1c174..b8084a70046 100644
--- a/spec/models/concerns/schedulable_spec.rb
+++ b/spec/models/concerns/schedulable_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe Schedulable do
end.new
end
- it 'works' do
+ it 'raises a NotImplementedError' do
expect { schedulable_instance.set_next_run_at }.to raise_error(NotImplementedError)
end
end
diff --git a/spec/models/concerns/sensitive_serializable_hash_spec.rb b/spec/models/concerns/sensitive_serializable_hash_spec.rb
index 3c9199ce18f..591a4383a03 100644
--- a/spec/models/concerns/sensitive_serializable_hash_spec.rb
+++ b/spec/models/concerns/sensitive_serializable_hash_spec.rb
@@ -35,12 +35,6 @@ RSpec.describe SensitiveSerializableHash do
expect(model.serializable_hash).not_to include('super_secret')
end
- context 'unsafe_serialization_hash option' do
- it 'includes the field in serializable_hash' do
- expect(model.serializable_hash(unsafe_serialization_hash: true)).to include('super_secret')
- end
- end
-
it 'does not change parent class attributes_exempt_from_serializable_hash' do
expect(test_class.attributes_exempt_from_serializable_hash).to contain_exactly(:super_secret)
expect(another_class.attributes_exempt_from_serializable_hash).to contain_exactly(:sub_secret)
@@ -65,21 +59,6 @@ RSpec.describe SensitiveSerializableHash do
expect(model.as_json).not_to include(attribute)
end
end
-
- context 'unsafe_serialization_hash option' do
- it 'includes the field in serializable_hash' do
- attributes.each do |attribute|
- expect(model.attributes).to include(attribute) # double-check the attribute does exist
-
- # Do not expect binary columns to appear in JSON
- next if klass.columns_hash[attribute]&.type == :binary
-
- expect(model.serializable_hash(unsafe_serialization_hash: true)).to include(attribute)
- expect(model.to_json(unsafe_serialization_hash: true)).to include(attribute)
- expect(model.as_json(unsafe_serialization_hash: true)).to include(attribute)
- end
- end
- end
end
end
@@ -120,18 +99,6 @@ RSpec.describe SensitiveSerializableHash do
expect(model.as_json).not_to include(attribute)
end
end
-
- context 'unsafe_serialization_hash option' do
- it 'includes the field in serializable_hash' do
- attributes.each do |attribute|
- expect(model.attributes).to include(attribute) # double-check the attribute does exist
-
- expect(model.serializable_hash(unsafe_serialization_hash: true)).to include(attribute)
- expect(model.to_json(unsafe_serialization_hash: true)).to include(attribute)
- expect(model.as_json(unsafe_serialization_hash: true)).to include(attribute)
- end
- end
- end
end
end
diff --git a/spec/models/concerns/signature_type_spec.rb b/spec/models/concerns/signature_type_spec.rb
new file mode 100644
index 00000000000..d8e2b617e0e
--- /dev/null
+++ b/spec/models/concerns/signature_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SignatureType do
+ describe '#type' do
+ context 'when class does not define a type method' do
+ subject(:implementation) { Class.new.include(described_class).new }
+
+ it 'raises a NoMethodError with custom message' do
+ expect { implementation.type }.to raise_error(NoMethodError, 'must implement `type` method')
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
index e0ebb86585a..2df86804f34 100644
--- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
@@ -237,6 +237,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
expect(subject.set_token(instance, 'my-value')).to eq 'my-value'
end
end
+
context 'when encryption is optional' do
let(:options) { { encrypted: :optional } }
diff --git a/spec/models/concerns/triggerable_hooks_spec.rb b/spec/models/concerns/triggerable_hooks_spec.rb
index 90c88c888ff..5682a189c41 100644
--- a/spec/models/concerns/triggerable_hooks_spec.rb
+++ b/spec/models/concerns/triggerable_hooks_spec.rb
@@ -4,8 +4,10 @@ require 'spec_helper'
RSpec.describe TriggerableHooks do
before do
- class TestableHook < WebHook
- include TriggerableHooks
+ stub_const('TestableHook', Class.new(WebHook))
+
+ TestableHook.class_eval do
+ include TriggerableHooks # rubocop:disable RSpec/DescribedClass
triggerable_hooks [:push_hooks]
end
end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 9af53bae204..33d3cabb325 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -879,10 +879,11 @@ RSpec.describe ContainerRepository, :aggregate_failures do
it 'updates deletion status attributes' do
expect { subject }.to change(repository, :status).from(nil).to('delete_ongoing')
.and change(repository, :delete_started_at).from(nil).to(Time.zone.now)
+ .and change(repository, :status_updated_at).from(nil).to(Time.zone.now)
end
end
- describe '#set_delete_scheduled_status' do
+ describe '#set_delete_scheduled_status', :freeze_time do
let_it_be(:repository) { create(:container_repository, :status_delete_ongoing, delete_started_at: 3.minutes.ago) }
subject { repository.set_delete_scheduled_status }
@@ -890,6 +891,27 @@ RSpec.describe ContainerRepository, :aggregate_failures do
it 'updates delete attributes' do
expect { subject }.to change(repository, :status).from('delete_ongoing').to('delete_scheduled')
.and change(repository, :delete_started_at).to(nil)
+ .and change(repository, :status_updated_at).to(Time.zone.now)
+ end
+ end
+
+ describe '#status_updated_at', :freeze_time do
+ let_it_be_with_reload(:repository) { create(:container_repository) }
+
+ %i[delete_scheduled delete_ongoing delete_failed].each do |status|
+ context "when status is updated to #{status}" do
+ it 'updates status_changed_at' do
+ expect { repository.update!(status: status) }.to change(repository, :status_updated_at).from(nil).to(Time.zone.now)
+ end
+ end
+ end
+
+ context 'when status is not changed' do
+ it 'does not update status_changed_at' do
+ repository.name = 'different-image'
+
+ expect { repository.save! }.not_to change(repository, :status_updated_at)
+ end
end
end
@@ -1632,7 +1654,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do
stub_application_setting(container_registry_import_target_plan: valid_container_repository.migration_plan)
end
- it 'works' do
+ it 'returns valid container repositories' do
expect(subject).to contain_exactly(valid_container_repository, valid_container_repository2)
end
end
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
index 04763accc42..1b8dd62455e 100644
--- a/spec/models/deploy_token_spec.rb
+++ b/spec/models/deploy_token_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DeployToken do
+RSpec.describe DeployToken, feature_category: :continuous_delivery do
subject(:deploy_token) { create(:deploy_token) }
it { is_expected.to have_many :project_deploy_tokens }
@@ -82,9 +82,7 @@ RSpec.describe DeployToken do
describe '#ensure_at_least_one_scope' do
context 'with at least one scope' do
- it 'is valid' do
- is_expected.to be_valid
- end
+ it { is_expected.to be_valid }
end
context 'with no scopes' do
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index daa65f528e9..fb3883820fd 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -925,6 +925,21 @@ RSpec.describe Deployment do
end
end
+ describe '#triggered_by?' do
+ subject { deployment.triggered_by?(user) }
+
+ let(:user) { create(:user) }
+ let(:deployment) { create(:deployment, user: user) }
+
+ it { is_expected.to eq(true) }
+
+ context 'when deployment triggerer is different' do
+ let(:deployment) { create(:deployment) }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
describe '.find_successful_deployment!' do
it 'returns a successful deployment' do
deploy = create(:deployment, :success)
diff --git a/spec/models/design_management/version_spec.rb b/spec/models/design_management/version_spec.rb
index 44ecae82174..8c0d7e99ae5 100644
--- a/spec/models/design_management/version_spec.rb
+++ b/spec/models/design_management/version_spec.rb
@@ -232,7 +232,7 @@ RSpec.describe DesignManagement::Version do
context 'there is a single design' do
let_it_be(:design) { create(:design) }
- shared_examples :a_correctly_categorised_design do |kind, category|
+ shared_examples 'a correctly categorised design' do |kind, category|
let_it_be(:version) { create(:design_version, kind => [design]) }
it 'returns a hash with a single key and the single design in that bucket' do
@@ -240,9 +240,9 @@ RSpec.describe DesignManagement::Version do
end
end
- it_behaves_like :a_correctly_categorised_design, :created_designs, 'creation'
- it_behaves_like :a_correctly_categorised_design, :modified_designs, 'modification'
- it_behaves_like :a_correctly_categorised_design, :deleted_designs, 'deletion'
+ it_behaves_like 'a correctly categorised design', :created_designs, 'creation'
+ it_behaves_like 'a correctly categorised design', :modified_designs, 'modification'
+ it_behaves_like 'a correctly categorised design', :deleted_designs, 'deletion'
end
context 'there are a bunch of different designs in a variety of states' do
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 8a3d43f58e0..2670127442e 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -64,6 +64,34 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
end
+ describe 'preloading deployment associations' do
+ let!(:environment) { create(:environment, project: project) }
+
+ associations = [:last_deployment, :last_visible_deployment, :upcoming_deployment]
+ associations.concat Deployment::FINISHED_STATUSES.map { |status| "last_#{status}_deployment".to_sym }
+ associations.concat Deployment::UPCOMING_STATUSES.map { |status| "last_#{status}_deployment".to_sym }
+
+ context 'raises error for legacy approach' do
+ let!(:error_pattern) { /Preloading instance dependent scopes is not supported/ }
+
+ subject { described_class.preload(association_name).find_by(id: environment) }
+
+ shared_examples 'raises error' do
+ it do
+ expect { subject }.to raise_error(error_pattern)
+ end
+ end
+
+ associations.each do |association|
+ context association.to_s do
+ let!(:association_name) { association }
+
+ include_examples "raises error"
+ end
+ end
+ end
+ end
+
describe 'validate and sanitize external url' do
let_it_be_with_refind(:environment) { create(:environment) }
@@ -184,7 +212,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
describe ".schedule_to_delete" do
- subject { described_class.for_id(deletable_environment).schedule_to_delete }
+ subject { described_class.id_in(deletable_environment).schedule_to_delete }
it "schedules the record for deletion" do
freeze_time do
@@ -282,6 +310,72 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
end
+ describe '.for_name_like_within_folder' do
+ subject { project.environments.for_name_like_within_folder(query, limit: limit) }
+
+ let!(:environment) { create(:environment, name: 'review/test-app', project: project) }
+ let!(:environment_a) { create(:environment, name: 'test-app', project: project) }
+ let(:query) { 'test' }
+ let(:limit) { 5 }
+
+ it 'returns a found name' do
+ is_expected.to contain_exactly(environment)
+ end
+
+ it 'does not return environment without folder' do
+ is_expected.not_to include(environment_a)
+ end
+
+ context 'when query is test-app' do
+ let(:query) { 'test-app' }
+
+ it 'returns a found name' do
+ is_expected.to include(environment)
+ end
+ end
+
+ context 'when query is test-app-a' do
+ let(:query) { 'test-app-a' }
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when query is empty string' do
+ let(:query) { '' }
+ let!(:environment_b) { create(:environment, name: 'review/test-app-1', project: project) }
+
+ it 'returns only the foldered environments' do
+ is_expected.to contain_exactly(environment, environment_b)
+ end
+ end
+
+ context 'when query is nil' do
+ let(:query) {}
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(NoMethodError)
+ end
+ end
+
+ context 'when query is partially matched in the middle of environment name' do
+ let(:query) { 'app' }
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when query contains a wildcard character' do
+ let(:query) { 'test%' }
+
+ it 'prevents wildcard injection' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '.auto_stoppable' do
subject { described_class.auto_stoppable(limit) }
@@ -1916,4 +2010,23 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
end
end
+
+ describe '#deploy_freezes' do
+ let(:environment) { create(:environment, project: project, name: 'staging') }
+ let(:freeze_period) { create(:ci_freeze_period, project: project) }
+
+ subject { environment.deploy_freezes }
+
+ it 'returns the freeze periods of the associated project' do
+ expect(subject).to contain_exactly(freeze_period)
+ end
+
+ it 'caches the freeze periods' do
+ expect(Gitlab::SafeRequestStore).to receive(:fetch)
+ .at_least(:once)
+ .and_return([freeze_period])
+
+ subject
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 9579c4c2d27..667f3ddff72 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe Event do
+RSpec.describe Event, feature_category: :users do
+ let_it_be_with_reload(:project) { create(:project) }
+
describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:target) }
@@ -17,8 +19,6 @@ RSpec.describe Event do
end
describe 'Callbacks' do
- let(:project) { create(:project) }
-
describe 'after_create :reset_project_activity' do
it 'calls the reset_project_activity method' do
expect_next_instance_of(described_class) do |instance|
@@ -105,7 +105,7 @@ RSpec.describe Event do
describe 'created_at' do
it 'can find the right event' do
time = 1.day.ago
- event = create(:event, created_at: time)
+ event = create(:event, created_at: time, project: project)
false_positive = create(:event, created_at: 2.days.ago)
found = described_class.created_at(time)
@@ -116,11 +116,11 @@ RSpec.describe Event do
end
describe '.for_fingerprint' do
- let_it_be(:with_fingerprint) { create(:event, fingerprint: 'aaa') }
+ let_it_be(:with_fingerprint) { create(:event, fingerprint: 'aaa', project: project) }
before_all do
- create(:event)
- create(:event, fingerprint: 'bbb')
+ create(:event, project: project)
+ create(:event, fingerprint: 'bbb', project: project)
end
it 'returns none if there is no fingerprint' do
@@ -137,28 +137,43 @@ RSpec.describe Event do
.to contain_exactly(with_fingerprint)
end
end
+
+ describe '.contributions' do
+ let!(:merge_request_event) { create(:event, :created, :for_merge_request, project: project) }
+ let!(:issue_event) { create(:event, :created, :for_issue, project: project) }
+ let!(:work_item_event) { create(:event, :created, :for_work_item, project: project) }
+ let!(:design_event) { create(:design_event, project: project) }
+
+ it 'returns events for MergeRequest, Issue and WorkItem' do
+ expect(described_class.contributions).to contain_exactly(
+ merge_request_event,
+ issue_event,
+ work_item_event
+ )
+ end
+ end
end
describe '#fingerprint' do
it 'is unique scoped to target' do
- issue = create(:issue)
- mr = create(:merge_request)
+ issue = create(:issue, project: project)
+ mr = create(:merge_request, source_project: project)
- expect { create_list(:event, 2, target: issue, fingerprint: '1234') }
+ expect { create_list(:event, 2, target: issue, fingerprint: '1234', project: project) }
.to raise_error(include('fingerprint'))
expect do
- create(:event, target: mr, fingerprint: 'abcd')
- create(:event, target: issue, fingerprint: 'abcd')
- create(:event, target: issue, fingerprint: 'efgh')
+ create(:event, target: mr, fingerprint: 'abcd', project: project)
+ create(:event, target: issue, fingerprint: 'abcd', project: project)
+ create(:event, target: issue, fingerprint: 'efgh', project: project)
end.not_to raise_error
end
end
describe "Push event" do
- let(:project) { create(:project, :private) }
- let(:user) { project.first_owner }
- let(:event) { create_push_event(project, user) }
+ let(:private_project) { create(:project, :private) }
+ let(:user) { private_project.first_owner }
+ let(:event) { create_push_event(private_project, user) }
it do
expect(event.push_action?).to be_truthy
@@ -171,8 +186,6 @@ RSpec.describe Event do
end
describe '#target_title' do
- let_it_be(:project) { create(:project) }
-
let(:author) { project.first_owner }
let(:target) { nil }
@@ -303,11 +316,11 @@ RSpec.describe Event do
end
def visible_to_none_except(*roles)
- visible_to_none.merge(roles.to_h { |role| [role, true] })
+ visible_to_none.merge(roles.index_with { true })
end
def visible_to_all_except(*roles)
- visible_to_all.merge(roles.to_h { |role| [role, false] })
+ visible_to_all.merge(roles.index_with { false })
end
shared_examples 'visibility examples' do
diff --git a/spec/models/factories_spec.rb b/spec/models/factories_spec.rb
index 65b993cca7f..4915c0bd870 100644
--- a/spec/models/factories_spec.rb
+++ b/spec/models/factories_spec.rb
@@ -44,6 +44,8 @@ RSpec.describe 'factories', :saas do
[:ci_pipeline_artifact, :remote_store],
# EE
[:dast_profile, :with_dast_site_validation],
+ [:dependency_proxy_manifest, :remote_store],
+ [:geo_dependency_proxy_manifest_state, any],
[:ee_ci_build, :dependency_scanning_report],
[:ee_ci_build, :license_scan_v1],
[:ee_ci_job_artifact, :v1],
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 9d70019734b..fe22b20ecf9 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe GenericCommitStatus do
end
describe '#name_uniqueness_across_types' do
- let(:attributes) { {} }
+ let(:attributes) { { context: 'default' } }
let(:commit_status) { described_class.new(attributes) }
let(:status_name) { 'test-job' }
@@ -39,7 +39,7 @@ RSpec.describe GenericCommitStatus do
end
context 'with only a pipeline' do
- let(:attributes) { { pipeline: pipeline } }
+ let(:attributes) { { pipeline: pipeline, context: 'default' } }
context 'without name' do
it_behaves_like 'it does not have uniqueness errors'
@@ -129,32 +129,6 @@ RSpec.describe GenericCommitStatus do
end
end
- describe 'set_default_values' do
- before do
- generic_commit_status.context = nil
- generic_commit_status.stage = nil
- generic_commit_status.save!
- end
-
- describe '#context' do
- subject { generic_commit_status.context }
-
- it { is_expected.not_to be_nil }
- end
-
- describe '#stage' do
- subject { generic_commit_status.stage }
-
- it { is_expected.not_to be_nil }
- end
-
- describe '#stage_idx' do
- subject { generic_commit_status.stage_idx }
-
- it { is_expected.not_to be_nil }
- end
- end
-
describe '#present' do
subject { generic_commit_status.present }
diff --git a/spec/models/group_deploy_key_spec.rb b/spec/models/group_deploy_key_spec.rb
index dfb4fee593f..3480e72c226 100644
--- a/spec/models/group_deploy_key_spec.rb
+++ b/spec/models/group_deploy_key_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
RSpec.describe GroupDeployKey do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:group_deploy_key) { create(:group_deploy_key) }
+
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:groups) }
- let_it_be(:group_deploy_key) { create(:group_deploy_key) }
- let_it_be(:group) { create(:group) }
-
it 'is of type DeployKey' do
expect(build(:group_deploy_key).type).to eq('DeployKey')
end
@@ -30,6 +30,12 @@ RSpec.describe GroupDeployKey do
end
end
+ describe '.defined_enums' do
+ it 'excludes the inherited enum' do
+ expect(described_class.defined_enums).to eq({})
+ end
+ end
+
describe '#can_be_edited_for' do
let_it_be(:user) { create(:user) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 6ba450b6d57..dfba0470d35 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe Group do
it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') }
it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') }
it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') }
- it { is_expected.to have_many(:protected_branches) }
+ it { is_expected.to have_many(:protected_branches).inverse_of(:group).with_foreign_key(:namespace_id) }
it { is_expected.to have_one(:crm_settings) }
it { is_expected.to have_one(:group_feature) }
it { is_expected.to have_one(:harbor_integration) }
@@ -842,10 +842,12 @@ RSpec.describe Group do
it 'returns matching records based on paths' do
expect(described_class.by_ids_or_paths(nil, [group_path])).to match_array([group])
+ expect(described_class.by_ids_or_paths(nil, [group_path.upcase])).to match_array([group])
end
it 'returns matching records based on ids' do
expect(described_class.by_ids_or_paths([group_id], nil)).to match_array([group])
+ expect(described_class.by_ids_or_paths([group_id], [])).to match_array([group])
end
it 'returns matching records based on both paths and ids' do
@@ -853,6 +855,13 @@ RSpec.describe Group do
expect(described_class.by_ids_or_paths([new_group.id], [group_path])).to match_array([group, new_group])
end
+
+ it 'returns matching records based on full_paths' do
+ new_group = create(:group, parent: group)
+
+ expect(described_class.by_ids_or_paths(nil, [new_group.full_path])).to match_array([new_group])
+ expect(described_class.by_ids_or_paths(nil, [new_group.full_path.upcase])).to match_array([new_group])
+ end
end
describe 'accessible_to_user' do
@@ -1955,6 +1964,78 @@ RSpec.describe Group do
end
end
+ describe '#self_and_hierarchy_intersecting_with_user_groups' do
+ let_it_be(:user) { create(:user) }
+ let(:subject) { group.self_and_hierarchy_intersecting_with_user_groups(user) }
+
+ it 'makes a call to GroupsFinder' do
+ expect(GroupsFinder).to receive_message_chain(:new, :execute, :unscope)
+
+ subject
+ end
+
+ context 'when the group is private' do
+ let_it_be(:group) { create(:group, :private) }
+
+ context 'when the user is not a member of the group' do
+ it 'is an empty array' do
+ expect(subject).to eq([])
+ end
+ end
+
+ context 'when the user is a member of the group' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'is equal to the group' do
+ expect(subject).to match_array([group])
+ end
+ end
+
+ context 'when the group has a sub group' do
+ let_it_be(:subgroup) { create(:group, :private, parent: group) }
+
+ context 'when the user is not a member of the subgroup' do
+ it 'is an empty array' do
+ expect(subject).to eq([])
+ end
+ end
+
+ context 'when the user is a member of the subgroup' do
+ before do
+ subgroup.add_developer(user)
+ end
+
+ it 'is equal to the group and subgroup' do
+ expect(subject).to match_array([group, subgroup])
+ end
+
+ context 'when the group has an ancestor' do
+ let_it_be(:ancestor) { create(:group, :private) }
+
+ before do
+ group.parent = ancestor
+ group.save!
+ end
+
+ it 'is equal to the ancestor, group and subgroup' do
+ expect(subject).to match_array([ancestor, group, subgroup])
+ end
+ end
+ end
+ end
+ end
+
+ context 'when the group is public' do
+ let_it_be(:group) { create(:group, :public) }
+
+ it 'is equal to the public group regardless of membership' do
+ expect(subject).to match_array([group])
+ end
+ end
+ end
+
describe '#update_two_factor_requirement_for_members' do
let_it_be_with_reload(:user) { create(:user) }
@@ -2735,7 +2816,7 @@ RSpec.describe Group do
end
describe 'has_project_with_service_desk_enabled?' do
- let_it_be(:group) { create(:group, :private) }
+ let_it_be_with_refind(:group) { create(:group, :private) }
subject { group.has_project_with_service_desk_enabled? }
@@ -3294,6 +3375,13 @@ RSpec.describe Group do
end
end
+ describe '#work_items_mvc_feature_flag_enabled?' do
+ it_behaves_like 'checks self and root ancestor feature flag' do
+ let(:feature_flag) { :work_items_mvc }
+ let(:feature_flag_method) { :work_items_mvc_feature_flag_enabled? }
+ end
+ end
+
describe '#work_items_mvc_2_feature_flag_enabled?' do
it_behaves_like 'checks self and root ancestor feature flag' do
let(:feature_flag) { :work_items_mvc_2 }
diff --git a/spec/models/hooks/active_hook_filter_spec.rb b/spec/models/hooks/active_hook_filter_spec.rb
index 47c0fbdb106..d2a8f89041e 100644
--- a/spec/models/hooks/active_hook_filter_spec.rb
+++ b/spec/models/hooks/active_hook_filter_spec.rb
@@ -85,23 +85,5 @@ RSpec.describe ActiveHookFilter do
it { expect(filter.matches?(:push_hooks, { ref: 'refs/heads/feature1' })).to be true }
end
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(enhanced_webhook_support_regex: false)
- end
-
- let(:hook) do
- build(
- :project_hook,
- push_events: true,
- push_events_branch_filter: '(master)',
- branch_filter_strategy: 'regex'
- )
- end
-
- it { expect(filter.matches?(:push_hooks, { ref: 'refs/heads/master' })).to be false }
- it { expect(filter.matches?(:push_hooks, { ref: 'refs/heads/(master)' })).to be true }
- end
end
end
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 68c284a913c..2ece04c7158 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -11,6 +11,32 @@ RSpec.describe ServiceHook do
it { is_expected.to validate_presence_of(:integration) }
end
+ describe 'executable?' do
+ let!(:hooks) do
+ [
+ [0, Time.current],
+ [0, 1.minute.from_now],
+ [1, 1.minute.from_now],
+ [3, 1.minute.from_now],
+ [4, nil],
+ [4, 1.day.ago],
+ [4, 1.minute.from_now],
+ [0, nil],
+ [0, 1.day.ago],
+ [1, nil],
+ [1, 1.day.ago],
+ [3, nil],
+ [3, 1.day.ago]
+ ].map do |(recent_failures, disabled_until)|
+ create(:service_hook, recent_failures: recent_failures, disabled_until: disabled_until)
+ end
+ end
+
+ it 'is always true' do
+ expect(hooks).to all(be_executable)
+ end
+ end
+
describe 'execute' do
let(:hook) { build(:service_hook) }
let(:data) { { key: 'value' } }
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 9b55db15f3b..994d5688808 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe WebHook do
it { is_expected.to allow_value({ 'MY_TOKEN' => 'bar' }).for(:url_variables) }
it { is_expected.to allow_value({ 'foo2' => 'bar' }).for(:url_variables) }
it { is_expected.to allow_value({ 'x' => 'y' }).for(:url_variables) }
- it { is_expected.to allow_value({ 'x' => ('a' * 100) }).for(:url_variables) }
+ it { is_expected.to allow_value({ 'x' => ('a' * 2048) }).for(:url_variables) }
it { is_expected.to allow_value({ 'foo' => 'bar', 'bar' => 'baz' }).for(:url_variables) }
it { is_expected.to allow_value((1..20).to_h { ["k#{_1}", 'value'] }).for(:url_variables) }
it { is_expected.to allow_value({ 'MY-TOKEN' => 'bar' }).for(:url_variables) }
@@ -45,7 +45,7 @@ RSpec.describe WebHook do
it { is_expected.not_to allow_value({ 'bar' => :baz }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'bar' => nil }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'foo' => '' }).for(:url_variables) }
- it { is_expected.not_to allow_value({ 'foo' => ('a' * 101) }).for(:url_variables) }
+ it { is_expected.not_to allow_value({ 'foo' => ('a' * 2049) }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'has spaces' => 'foo' }).for(:url_variables) }
it { is_expected.not_to allow_value({ '' => 'foo' }).for(:url_variables) }
it { is_expected.not_to allow_value({ '1foo' => 'foo' }).for(:url_variables) }
@@ -237,16 +237,6 @@ RSpec.describe WebHook do
it { is_expected.to contain_exactly(:token, :url, :url_variables) }
end
- describe '.web_hooks_disable_failed?' do
- it 'returns true when feature is enabled for parent' do
- second_hook = build(:project_hook)
- stub_feature_flags(web_hooks_disable_failed: [false, second_hook.project])
-
- expect(described_class.web_hooks_disable_failed?(hook)).to eq(false)
- expect(described_class.web_hooks_disable_failed?(second_hook)).to eq(true)
- end
- end
-
describe 'execute' do
let(:data) { { key: 'value' } }
let(:hook_name) { 'project hook' }
@@ -327,16 +317,6 @@ RSpec.describe WebHook do
expect(described_class.where(project_id: project.id).executable).to match_array executables
expect(described_class.where(project_id: project.id).disabled).to match_array not_executable
end
-
- context 'when the feature flag is not enabled' do
- before do
- stub_feature_flags(web_hooks_disable_failed: false)
- end
-
- specify 'enabled is the same as all' do
- expect(described_class.where(project_id: project.id).executable).to match_array(executables + not_executable)
- end
- end
end
describe '#executable?' do
@@ -384,26 +364,6 @@ RSpec.describe WebHook do
it 'has the correct state' do
expect(web_hook.executable?).to eq(executable)
end
-
- context 'when the feature flag is enabled for a project' do
- before do
- stub_feature_flags(web_hooks_disable_failed: project)
- end
-
- it 'has the expected value' do
- expect(web_hook.executable?).to eq(executable)
- end
- end
-
- context 'when the feature flag is not enabled' do
- before do
- stub_feature_flags(web_hooks_disable_failed: false)
- end
-
- it 'is executable' do
- expect(web_hook).to be_executable
- end
- end
end
end
@@ -643,12 +603,6 @@ RSpec.describe WebHook do
it 'is true' do
expect(hook).to be_temporarily_disabled
end
-
- it 'is false when `web_hooks_disable_failed` flag is disabled' do
- stub_feature_flags(web_hooks_disable_failed: false)
-
- expect(hook).not_to be_temporarily_disabled
- end
end
end
@@ -665,12 +619,6 @@ RSpec.describe WebHook do
it 'is true' do
expect(hook).to be_permanently_disabled
end
-
- it 'is false when `web_hooks_disable_failed` flag is disabled' do
- stub_feature_flags(web_hooks_disable_failed: false)
-
- expect(hook).not_to be_permanently_disabled
- end
end
end
@@ -730,17 +678,9 @@ RSpec.describe WebHook do
expect { hook.to_json }.not_to raise_error
end
- it 'does not error, when serializing unsafe attributes' do
- expect { hook.to_json(unsafe_serialization_hash: true) }.not_to raise_error
- end
-
it 'does not contain binary attributes' do
expect(hook.to_json).not_to include('encrypted_url_variables')
end
-
- it 'does not contain binary attributes, even when serializing unsafe attributes' do
- expect(hook.to_json(unsafe_serialization_hash: true)).not_to include('encrypted_url_variables')
- end
end
describe '#interpolated_url' do
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index 4938e1797af..78b30221a24 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -1303,8 +1303,8 @@ RSpec.describe Integration do
describe '#async_execute' do
let(:integration) { described_class.new(id: 123) }
- let(:data) { { object_kind: 'push' } }
- let(:supported_events) { %w[push] }
+ let(:data) { { object_kind: 'build' } }
+ let(:supported_events) { %w[push build] }
subject(:async_execute) { integration.async_execute(data) }
diff --git a/spec/models/integrations/base_chat_notification_spec.rb b/spec/models/integrations/base_chat_notification_spec.rb
index b959ead2cae..67fc09fd8b5 100644
--- a/spec/models/integrations/base_chat_notification_spec.rb
+++ b/spec/models/integrations/base_chat_notification_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Integrations::BaseChatNotification do
webhook: webhook_url
)
- WebMock.stub_request(:post, webhook_url)
+ WebMock.stub_request(:post, webhook_url) if webhook_url.present?
subject.active = true
end
@@ -55,6 +55,33 @@ RSpec.describe Integrations::BaseChatNotification do
end
end
+ context 'when webhook is blank' do
+ let(:webhook_url) { '' }
+
+ it 'returns false' do
+ expect(chat_integration).not_to receive(:notify)
+ expect(chat_integration.execute(data)).to be false
+ end
+
+ context 'when webhook is not required' do
+ it 'returns true' do
+ allow(chat_integration).to receive(:requires_webhook?).and_return(false)
+
+ expect(chat_integration).to receive(:notify).and_return(true)
+ expect(chat_integration.execute(data)).to be true
+ end
+ end
+ end
+
+ context 'when event is not supported' do
+ it 'returns false' do
+ allow(chat_integration).to receive(:supported_events).and_return(['foo'])
+
+ expect(chat_integration).not_to receive(:notify)
+ expect(chat_integration.execute(data)).to be false
+ end
+ end
+
context 'with a project with name containing spaces' do
it 'does not remove spaces' do
allow(project).to receive(:full_name).and_return('Project Name')
diff --git a/spec/models/integrations/base_slack_notification_spec.rb b/spec/models/integrations/base_slack_notification_spec.rb
new file mode 100644
index 00000000000..8f7f4e8858d
--- /dev/null
+++ b/spec/models/integrations/base_slack_notification_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::BaseSlackNotification do
+ # This spec should only contain tests that cannot be tested through
+ # `base_slack_notification_shared_examples.rb`.
+
+ describe '#metrics_key_prefix (private method)' do
+ it 'raises a NotImplementedError error when not defined' do
+ subclass = Class.new(described_class)
+
+ expect { subclass.new.send(:metrics_key_prefix) }.to raise_error(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/models/integrations/chat_message/pipeline_message_spec.rb b/spec/models/integrations/chat_message/pipeline_message_spec.rb
index f3388853b37..413cb097327 100644
--- a/spec/models/integrations/chat_message/pipeline_message_spec.rb
+++ b/spec/models/integrations/chat_message/pipeline_message_spec.rb
@@ -40,8 +40,6 @@ RSpec.describe Integrations::ChatMessage::PipelineMessage do
let(:has_yaml_errors) { false }
- it_behaves_like Integrations::ChatMessage
-
before do
test_commit = double("A test commit", committer: args[:user], title: "A test commit message")
test_project = build(:project, name: args[:project][:name])
@@ -62,6 +60,8 @@ RSpec.describe Integrations::ChatMessage::PipelineMessage do
allow(Gitlab::UrlBuilder).to receive(:build).with(args[:user]).and_return("http://example.gitlab.com/hacker")
end
+ it_behaves_like Integrations::ChatMessage
+
it 'returns an empty pretext' do
expect(subject.pretext).to be_empty
end
diff --git a/spec/models/integrations/drone_ci_spec.rb b/spec/models/integrations/drone_ci_spec.rb
index 6ff6888e0d3..c46face9702 100644
--- a/spec/models/integrations/drone_ci_spec.rb
+++ b/spec/models/integrations/drone_ci_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
end
end
- shared_context :drone_ci_integration do
+ shared_context 'drone ci integration' do
subject(:drone) do
described_class.new(
project: project,
@@ -116,7 +116,7 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
end
it_behaves_like Integrations::HasWebHook do
- include_context :drone_ci_integration
+ include_context 'drone ci integration'
let(:integration) { drone }
let(:hook_url) { "#{drone_url}/hook?owner=#{project.namespace.full_path}&name=#{project.path}&access_token={token}" }
@@ -130,14 +130,14 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
end
describe "integration page/path methods" do
- include_context :drone_ci_integration
+ include_context 'drone ci integration'
it { expect(drone.build_page(sha, branch)).to eq(build_page) }
it { expect(drone.commit_status_path(sha, branch)).to eq(commit_status_path) }
end
describe '#commit_status' do
- include_context :drone_ci_integration
+ include_context 'drone ci integration'
it 'returns the contents of the reactive cache' do
stub_reactive_cache(drone, { commit_status: 'foo' }, 'sha', 'ref')
@@ -147,7 +147,7 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
end
describe '#calculate_reactive_cache' do
- include_context :drone_ci_integration
+ include_context 'drone ci integration'
describe '#commit_status' do
subject { drone.calculate_reactive_cache(sha, branch)[:commit_status] }
@@ -193,7 +193,7 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
end
describe "execute" do
- include_context :drone_ci_integration
+ include_context 'drone ci integration'
let(:user) { build(:user, username: 'username') }
let(:push_sample_data) do
diff --git a/spec/models/integrations/flowdock_spec.rb b/spec/models/integrations/flowdock_spec.rb
deleted file mode 100644
index daafb1b3958..00000000000
--- a/spec/models/integrations/flowdock_spec.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Integrations::Flowdock do
- describe 'Validations' do
- context 'when integration is active' do
- before do
- subject.active = true
- end
-
- it { is_expected.to validate_presence_of(:token) }
- end
-
- context 'when integration is inactive' do
- before do
- subject.active = false
- end
-
- it { is_expected.not_to validate_presence_of(:token) }
- end
- end
-
- describe "Execute" do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
- let(:api_url) { 'https://api.flowdock.com/v1/messages' }
-
- subject(:flowdock_integration) { described_class.new }
-
- before do
- allow(flowdock_integration).to receive_messages(
- project_id: project.id,
- project: project,
- token: 'verySecret'
- )
- WebMock.stub_request(:post, api_url)
- end
-
- it "calls FlowDock API" do
- flowdock_integration.execute(sample_data)
-
- sample_data[:commits].each do |commit|
- # One request to Flowdock per new commit
- next if commit[:id] == sample_data[:before]
-
- expect(WebMock).to have_requested(:post, api_url).with(
- body: /#{commit[:id]}.*#{project.path}/
- ).once
- end
- end
- end
-end
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index af1112cf50d..a4ccae459cf 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -469,7 +469,8 @@ RSpec.describe Integrations::Jira do
end
describe '#client' do
- subject do
+ it 'uses the default GitLab::HTTP timeouts' do
+ timeouts = Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS
stub_request(:get, 'http://jira.example.com/foo')
expect(Gitlab::HTTP).to receive(:httparty_perform_request)
@@ -477,32 +478,6 @@ RSpec.describe Integrations::Jira do
jira_integration.client.get('/foo')
end
-
- context 'when the FF :jira_raise_timeouts is enabled' do
- let(:timeouts) do
- {
- open_timeout: 2.minutes,
- read_timeout: 2.minutes,
- write_timeout: 2.minutes
- }
- end
-
- it 'uses custom timeouts' do
- subject
- end
- end
-
- context 'when the FF :jira_raise_timeouts is disabled' do
- before do
- stub_feature_flags(jira_raise_timeouts: false)
- end
-
- let(:timeouts) { Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS }
-
- it 'uses the default GitLab::HTTP timeouts' do
- subject
- end
- end
end
describe '#find_issue' do
@@ -612,7 +587,7 @@ RSpec.describe Integrations::Jira do
close_issue
end
- it_behaves_like 'Snowplow event tracking' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
subject { close_issue }
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
@@ -968,7 +943,7 @@ RSpec.describe Integrations::Jira do
subject
end
- it_behaves_like 'Snowplow event tracking' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { 'Integrations::Jira' }
let(:action) { 'perform_integrations_action' }
diff --git a/spec/models/integrations/slack_spec.rb b/spec/models/integrations/slack_spec.rb
index a12bc7f4831..218d92ffe05 100644
--- a/spec/models/integrations/slack_spec.rb
+++ b/spec/models/integrations/slack_spec.rb
@@ -4,5 +4,9 @@ require 'spec_helper'
RSpec.describe Integrations::Slack do
it_behaves_like Integrations::SlackMattermostNotifier, 'Slack'
- it_behaves_like Integrations::BaseSlackNotification, factory: :integrations_slack
+ it_behaves_like Integrations::BaseSlackNotification, factory: :integrations_slack do
+ before do
+ stub_request(:post, integration.webhook)
+ end
+ end
end
diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb
deleted file mode 100644
index 183082bab26..00000000000
--- a/spec/models/issue_collection_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe IssueCollection do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:issue1) { create(:issue, project: project) }
- let(:issue2) { create(:issue, project: project) }
- let(:collection) { described_class.new([issue1, issue2]) }
-
- describe '#collection' do
- it 'returns the issues in the same order as the input Array' do
- expect(collection.collection).to eq([issue1, issue2])
- end
- end
-
- describe '#updatable_by_user' do
- context 'using an admin user' do
- it 'returns all issues' do
- user = create(:admin)
-
- expect(collection.updatable_by_user(user)).to eq([issue1, issue2])
- end
- end
-
- context 'using a user that has no access to the project' do
- it 'returns no issues when the user is not an assignee or author' do
- expect(collection.updatable_by_user(user)).to be_empty
- end
-
- it 'returns the issues the user is assigned to' do
- issue1.assignees << user
-
- expect(collection.updatable_by_user(user)).to eq([issue1])
- end
-
- it 'returns the issues for which the user is the author' do
- issue1.author = user
-
- expect(collection.updatable_by_user(user)).to eq([issue1])
- end
- end
-
- context 'using a user that has reporter access to the project' do
- it 'returns the issues of the project' do
- project.add_reporter(user)
-
- expect(collection.updatable_by_user(user)).to eq([issue1, issue2])
- end
- end
-
- # TODO update when we have multiple owners of a project
- # https://gitlab.com/gitlab-org/gitlab/-/issues/350605
- context 'using a user that is an owner of a project' do
- it 'returns the issues of the project' do
- expect(collection.updatable_by_user(project.namespace.owner))
- .to eq([issue1, issue2])
- end
- end
- end
-
- describe '#visible_to' do
- it 'is an alias for updatable_by_user' do
- updatable_by_user = described_class.instance_method(:updatable_by_user)
- visible_to = described_class.instance_method(:visible_to)
-
- expect(visible_to).to eq(updatable_by_user)
- end
- end
-end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index aea8bdaf343..7c147067714 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issue do
+RSpec.describe Issue, feature_category: :project_management do
include ExternalAuthorizationServiceHelpers
using RSpec::Parameterized::TableSyntax
@@ -25,7 +25,7 @@ RSpec.describe Issue do
it { is_expected.to have_many(:design_versions) }
it { is_expected.to have_one(:sentry_issue) }
it { is_expected.to have_one(:alert_management_alert) }
- it { is_expected.to have_many(:alert_management_alerts) }
+ it { is_expected.to have_many(:alert_management_alerts).validate(false) }
it { is_expected.to have_many(:resource_milestone_events) }
it { is_expected.to have_many(:resource_state_events) }
it { is_expected.to have_and_belong_to_many(:prometheus_alert_events) }
@@ -138,6 +138,31 @@ RSpec.describe Issue do
end
end
+ describe '#allowed_work_item_type_change' do
+ where(:old_type, :new_type, :is_valid) do
+ :issue | :incident | true
+ :incident | :issue | true
+ :test_case | :issue | true
+ :issue | :test_case | true
+ :issue | :task | false
+ :test_case | :task | false
+ :incident | :task | false
+ :task | :issue | false
+ :task | :incident | false
+ :task | :test_case | false
+ end
+
+ with_them do
+ it 'is possible to change type only between selected types' do
+ issue = create(:issue, old_type, project: reusable_project)
+
+ issue.work_item_type_id = WorkItems::Type.default_by_type(new_type).id
+
+ expect(issue.valid?).to eq(is_valid)
+ end
+ end
+ end
+
describe 'confidentiality' do
let_it_be(:project) { create(:project) }
@@ -257,7 +282,7 @@ RSpec.describe Issue do
end
context 'when no type was set' do
- let_it_be(:issue, refind: true) { build(:issue, project: project, work_item_type: nil).tap { |issue| issue.save!(validate: false) } }
+ let(:issue) { build(:issue, project: project, work_item_type: nil) }
it 'sets a work item type before validation' do
expect(issue.work_item_type_id).to be_nil
@@ -449,17 +474,6 @@ RSpec.describe Issue do
end
end
- # TODO: Remove when NOT NULL constraint is added to the relationship
- describe '#work_item_type' do
- let(:issue) { build(:issue, :incident, project: reusable_project, work_item_type: nil).tap { |issue| issue.save!(validate: false) } }
-
- it 'returns a default type if the legacy issue does not have a work item type associated yet' do
- expect(issue.work_item_type_id).to be_nil
- expect(issue.issue_type).to eq('incident')
- expect(issue.work_item_type).to eq(WorkItems::Type.default_by_type(:incident))
- end
- end
-
describe '#sort' do
let(:project) { reusable_project }
diff --git a/spec/models/jira_connect_installation_spec.rb b/spec/models/jira_connect_installation_spec.rb
index 09a4a7a488c..525690fa6b7 100644
--- a/spec/models/jira_connect_installation_spec.rb
+++ b/spec/models/jira_connect_installation_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnectInstallation do
+RSpec.describe JiraConnectInstallation, feature_category: :integrations do
describe 'associations' do
it { is_expected.to have_many(:subscriptions).class_name('JiraConnectSubscription') }
end
@@ -124,6 +124,20 @@ RSpec.describe JiraConnectInstallation do
end
end
+ describe 'audience_uninstalled_event_url' do
+ let(:installation) { build(:jira_connect_installation) }
+
+ subject(:audience) { installation.audience_uninstalled_event_url }
+
+ it { is_expected.to eq(nil) }
+
+ context 'when proxy installation' do
+ let(:installation) { build(:jira_connect_installation, instance_url: 'https://example.com') }
+
+ it { is_expected.to eq('https://example.com/-/jira_connect/events/uninstalled') }
+ end
+ end
+
describe 'proxy?' do
let(:installation) { build(:jira_connect_installation) }
diff --git a/spec/models/jira_import_state_spec.rb b/spec/models/jira_import_state_spec.rb
index 95e9594f885..c31bedd08f3 100644
--- a/spec/models/jira_import_state_spec.rb
+++ b/spec/models/jira_import_state_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraImportState do
+RSpec.describe JiraImportState, feature_category: :integrations do
describe "associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index b98c0e8eae0..92f4d6d8531 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Key, :mailer do
+ it_behaves_like 'having unique enum values'
+
describe "Associations" do
it { is_expected.to belong_to(:user) }
end
@@ -216,6 +218,20 @@ RSpec.describe Key, :mailer do
end
end
end
+
+ context 'usage type scopes' do
+ let_it_be(:auth_key) { create(:key, usage_type: :auth) }
+ let_it_be(:auth_and_signing_key) { create(:key, usage_type: :auth_and_signing) }
+ let_it_be(:signing_key) { create(:key, usage_type: :signing) }
+
+ it 'auth scope returns auth and auth_and_signing keys' do
+ expect(described_class.auth).to match_array([auth_key, auth_and_signing_key])
+ end
+
+ it 'signing scope returns signing and auth_and_signing keys' do
+ expect(described_class.signing).to match_array([signing_key, auth_and_signing_key])
+ end
+ end
end
context 'validation of uniqueness (based on fingerprint uniqueness)' do
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index c25d0451f18..e38ffd97eb9 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -92,65 +92,13 @@ RSpec.describe LfsObject do
end
end
- describe '#schedule_background_upload' do
+ describe 'storage types' do
before do
stub_lfs_setting(enabled: true)
end
subject { create(:lfs_object, :with_file) }
- context 'when object storage is disabled' do
- before do
- stub_lfs_object_storage(enabled: false)
- end
-
- it 'does not schedule the migration' do
- expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
-
- subject
- end
- end
-
- context 'when object storage is enabled' do
- context 'when background upload is enabled' do
- context 'when is licensed' do
- before do
- stub_lfs_object_storage(background_upload: true)
- end
-
- it 'schedules the model for migration' do
- expect(ObjectStorage::BackgroundMoveWorker)
- .to receive(:perform_async)
- .with('LfsObjectUploader', described_class.name, :file, kind_of(Numeric))
- .once
-
- subject
- end
-
- it 'schedules the model for migration once' do
- expect(ObjectStorage::BackgroundMoveWorker)
- .to receive(:perform_async)
- .with('LfsObjectUploader', described_class.name, :file, kind_of(Numeric))
- .once
-
- create(:lfs_object, :with_file)
- end
- end
- end
-
- context 'when background upload is disabled' do
- before do
- stub_lfs_object_storage(background_upload: false)
- end
-
- it 'schedules the model for migration' do
- expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
-
- subject
- end
- end
- end
-
describe 'file is being stored' do
subject { create(:lfs_object, :with_file) }
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 2ecd10cccc6..0ebccf1cb65 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -175,26 +175,31 @@ RSpec.describe Member do
end
context 'member role access level' do
- let_it_be(:member) { create(:group_member, access_level: Gitlab::Access::DEVELOPER) }
+ let_it_be_with_reload(:member) { create(:group_member, access_level: Gitlab::Access::DEVELOPER) }
- context 'no member role is associated' do
+ context 'when no member role is associated' do
it 'is valid' do
expect(member).to be_valid
end
end
- context 'member role is associated' do
+ context 'when member role is associated' do
let!(:member_role) do
- create(:member_role, members: [member], base_access_level: Gitlab::Access::DEVELOPER)
+ create(
+ :member_role,
+ members: [member],
+ base_access_level: Gitlab::Access::DEVELOPER,
+ namespace: member.member_namespace
+ )
end
- context 'member role matches access level' do
+ context 'when member role matches access level' do
it 'is valid' do
expect(member).to be_valid
end
end
- context 'member role does not match access level' do
+ context 'when member role does not match access level' do
it 'is invalid' do
member_role.base_access_level = Gitlab::Access::MAINTAINER
@@ -202,13 +207,57 @@ RSpec.describe Member do
end
end
- context 'access_level cannot be changed' do
+ context 'when access_level is changed' do
it 'is invalid' do
member.access_level = Gitlab::Access::MAINTAINER
expect(member).not_to be_valid
- expect(member.errors.full_messages).to include(
- "Access level cannot be changed since member is associated with a custom role"
+ expect(member.errors[:access_level]).to include(
+ _("cannot be changed since member is associated with a custom role")
+ )
+ end
+ end
+ end
+ end
+
+ context 'member role namespace' do
+ let_it_be_with_reload(:member) { create(:group_member) }
+
+ context 'when no member role is associated' do
+ it 'is valid' do
+ expect(member).to be_valid
+ end
+ end
+
+ context 'when member role is associated' do
+ let_it_be(:member_role) do
+ create(:member_role, members: [member], namespace: member.group, base_access_level: member.access_level)
+ end
+
+ context 'when member#member_namespace is a group within hierarchy of member_role#namespace' do
+ it 'is valid' do
+ member.member_namespace = create(:group, parent: member_role.namespace)
+
+ expect(member).to be_valid
+ end
+ end
+
+ context 'when member#member_namespace is a project within hierarchy of member_role#namespace' do
+ it 'is valid' do
+ project = create(:project, group: member_role.namespace)
+ member.member_namespace = Namespace.find(project.parent_id)
+
+ expect(member).to be_valid
+ end
+ end
+
+ context 'when member#member_namespace is outside hierarchy of member_role#namespace' do
+ it 'is invalid' do
+ member.member_namespace = create(:group)
+
+ expect(member).not_to be_valid
+ expect(member.errors[:member_namespace]).to include(
+ _("must be in same hierarchy as custom role's namespace")
)
end
end
@@ -248,7 +297,7 @@ RSpec.describe Member do
accepted_invite_user = build(:user, state: :active)
@accepted_invite_member = create(:project_member, :invited, :developer, project: project)
- .tap { |u| u.accept_invite!(accepted_invite_user) }
+ .tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) }
@requested_member = project.requesters.find_by(user_id: requested_user.id)
@@ -612,14 +661,14 @@ RSpec.describe Member do
subject { described_class.authorizable.to_a }
it 'includes the member who has an associated user record,'\
- 'but also having an invite_token' do
- member = create(:project_member,
- :developer,
- :invited,
- user: create(:user))
+ 'but also having an invite_token' do
+ member = create(:project_member,
+ :developer,
+ :invited,
+ user: create(:user))
- expect(subject).to include(member)
- end
+ expect(subject).to include(member)
+ end
it { is_expected.to include @owner }
it { is_expected.to include @maintainer }
@@ -750,9 +799,35 @@ RSpec.describe Member do
end
describe '#request?' do
- subject { create(:project_member, requested_at: Time.current.utc) }
+ context 'when request for project' do
+ subject { create(:project_member, requested_at: Time.current.utc) }
+
+ it 'calls notification service but not todo service' do
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:new_access_request)
+ end
- it { is_expected.to be_request }
+ expect(TodoService).not_to receive(:new)
+
+ is_expected.to be_request
+ end
+ end
+
+ context 'when request for group' do
+ subject { create(:group_member, requested_at: Time.current.utc) }
+
+ it 'calls notification and todo service' do
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:new_access_request)
+ end
+
+ expect_next_instance_of(TodoService) do |instance|
+ expect(instance).to receive(:create_member_access_request)
+ end
+
+ is_expected.to be_request
+ end
+ end
end
describe '#pending?' do
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 77bc6d9753f..4ac7ce95b84 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -120,6 +120,40 @@ RSpec.describe GroupMember do
end
end
+ describe '#last_owner_of_the_group?' do
+ context 'when member is an owner' do
+ let_it_be(:group_member) { build(:group_member, :owner) }
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:member_last_owner?, :member_last_blocked_owner?, :expected) do
+ false | false | false
+ true | false | true
+ false | true | true
+ true | true | true
+ end
+
+ with_them do
+ it "returns expected" do
+ allow(group_member.group).to receive(:member_last_owner?).with(group_member).and_return(member_last_owner?)
+ allow(group_member.group).to receive(:member_last_blocked_owner?)
+ .with(group_member)
+ .and_return(member_last_blocked_owner?)
+
+ expect(group_member.last_owner_of_the_group?).to be(expected)
+ end
+ end
+ end
+
+ context 'when member is not an owner' do
+ let_it_be(:group_member) { build(:group_member, :guest) }
+
+ subject { group_member.last_owner_of_the_group? }
+
+ it { is_expected.to be(false) }
+ end
+ end
+
context 'access levels' do
context 'with parent group' do
it_behaves_like 'inherited access level as a member of entity' do
diff --git a/spec/models/members/member_role_spec.rb b/spec/models/members/member_role_spec.rb
index e2691e2e78c..f9d6757bb90 100644
--- a/spec/models/members/member_role_spec.rb
+++ b/spec/models/members/member_role_spec.rb
@@ -9,39 +9,50 @@ RSpec.describe MemberRole do
end
describe 'validation' do
- subject { described_class.new }
+ subject(:member_role) { build(:member_role) }
it { is_expected.to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:base_access_level) }
- context 'for namespace' do
- subject { build(:member_role) }
-
+ context 'when for namespace' do
let_it_be(:root_group) { create(:group) }
context 'when namespace is a subgroup' do
it 'is invalid' do
subgroup = create(:group, parent: root_group)
- subject.namespace = subgroup
+ member_role.namespace = subgroup
- expect(subject).to be_invalid
+ expect(member_role).to be_invalid
+ expect(member_role.errors[:namespace]).to include(
+ s_("MemberRole|must be top-level namespace")
+ )
end
end
context 'when namespace is a root group' do
it 'is valid' do
- subject.namespace = root_group
+ member_role.namespace = root_group
- expect(subject).to be_valid
+ expect(member_role).to be_valid
end
end
context 'when namespace is not present' do
it 'is invalid with a different error message' do
- subject.namespace = nil
+ member_role.namespace = nil
+
+ expect(member_role).to be_invalid
+ expect(member_role.errors[:namespace]).to include(_("can't be blank"))
+ end
+ end
+
+ context 'when namespace is outside hierarchy of member' do
+ it 'creates a validation error' do
+ member_role.save!
+ member_role.namespace = create(:group)
- expect(subject).to be_invalid
- expect(subject.errors.full_messages).to eq(["Namespace can't be blank"])
+ expect(member_role).not_to be_valid
+ expect(member_role.errors[:namespace]).to include(s_("MemberRole|can't be changed"))
end
end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index e56c6b38992..d573fde5a74 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -85,6 +85,27 @@ RSpec.describe ProjectMember do
end
end
+ describe '#holder_of_the_personal_namespace?' do
+ let_it_be(:project_member) { build(:project_member) }
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:personal_namespace_holder?, :expected) do
+ false | false
+ true | true
+ end
+
+ with_them do
+ it "returns expected" do
+ allow(project_member.project).to receive(:personal_namespace_holder?)
+ .with(project_member.user)
+ .and_return(personal_namespace_holder?)
+
+ expect(project_member.holder_of_the_personal_namespace?).to be(expected)
+ end
+ end
+ end
+
describe '.import_team' do
before do
@project_1 = create(:project)
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 22fed716897..a17b90930f0 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -507,6 +507,50 @@ RSpec.describe MergeRequestDiff do
end
end
+ describe '#paginated_diffs' do
+ context 'when no persisted files available' do
+ before do
+ diff_with_commits.clean!
+ end
+
+ it 'returns a Gitlab::Diff::FileCollection::Compare' do
+ diffs = diff_with_commits.paginated_diffs(1, 10)
+
+ expect(diffs).to be_a(Gitlab::Diff::FileCollection::Compare)
+ expect(diffs.diff_files.size).to eq(10)
+ end
+ end
+
+ context 'when persisted files available' do
+ it 'returns paginated diffs' do
+ diffs = diff_with_commits.paginated_diffs(1, 10)
+
+ expect(diffs).to be_a(Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff)
+ expect(diffs.diff_files.size).to eq(10)
+ end
+
+ it 'sorts diff files directory first' do
+ diff_with_commits.update!(sorted: false) # Mark as unsorted so it'll re-order
+
+ # There will be 11 returned, as we have to take into account for new and old paths
+ expect(diff_with_commits.paginated_diffs(1, 10).diff_paths).to eq(
+ [
+ 'bar/branch-test.txt',
+ 'custom-highlighting/test.gitlab-custom',
+ 'encoding/iso8859.txt',
+ 'files/images/wm.svg',
+ 'files/js/commit.js.coffee',
+ 'files/js/commit.coffee',
+ 'files/lfs/lfs_object.iso',
+ 'files/ruby/popen.rb',
+ 'files/ruby/regex.rb',
+ 'files/.DS_Store',
+ 'files/whitespace'
+ ])
+ end
+ end
+ end
+
describe '#diffs' do
let(:diff_options) { {} }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index cf4f58f558c..05586cbfc64 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -30,6 +30,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
it { is_expected.to have_many(:resource_state_events) }
it { is_expected.to have_many(:draft_notes) }
it { is_expected.to have_many(:reviews).inverse_of(:merge_request) }
+ it { is_expected.to have_many(:reviewed_by_users).through(:reviews).source(:author) }
it { is_expected.to have_one(:cleanup_schedule).inverse_of(:merge_request) }
it { is_expected.to have_many(:created_environments).class_name('Environment').inverse_of(:merge_request) }
@@ -46,6 +47,20 @@ RSpec.describe MergeRequest, factory_default: :keep do
expect(project.merge_requests.find(merge_request.id)).to eq(merge_request)
end
end
+
+ describe '#reviewed_by_users' do
+ let!(:merge_request) { create(:merge_request) }
+
+ context 'when the same user has several reviews' do
+ before do
+ 2.times { create(:review, merge_request: merge_request, project: merge_request.project, author: merge_request.author) }
+ end
+
+ it 'returns distinct users' do
+ expect(merge_request.reviewed_by_users).to match_array([merge_request.author])
+ end
+ end
+ end
end
describe '.from_and_to_forks' do
@@ -4229,6 +4244,18 @@ RSpec.describe MergeRequest, factory_default: :keep do
transition!
end
+
+ context 'when transaction is not committed' do
+ it_behaves_like 'transition not triggering mergeRequestMergeStatusUpdated GraphQL subscription' do
+ def transition!
+ MergeRequest.transaction do
+ super
+
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+ end
end
shared_examples 'for an invalid state transition' do
diff --git a/spec/models/milestone_note_spec.rb b/spec/models/milestone_note_spec.rb
index db1a7ca05f8..9371cef7540 100644
--- a/spec/models/milestone_note_spec.rb
+++ b/spec/models/milestone_note_spec.rb
@@ -20,6 +20,8 @@ RSpec.describe MilestoneNote do
it 'creates the expected note' do
expect(subject.note_html).to include('removed milestone')
expect(subject.note_html).not_to include('changed milestone to')
+ expect(subject.created_at).to eq(event.created_at)
+ expect(subject.updated_at).to eq(event.created_at)
end
end
end
diff --git a/spec/models/ml/candidate_metadata_spec.rb b/spec/models/ml/candidate_metadata_spec.rb
new file mode 100644
index 00000000000..94e21a910be
--- /dev/null
+++ b/spec/models/ml/candidate_metadata_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::CandidateMetadata, feature_category: :mlops do
+ describe 'associations' do
+ it { is_expected.to belong_to(:candidate) }
+ end
+
+ describe 'uniqueness of name' do
+ let_it_be(:metadata) { create(:ml_candidate_metadata, name: 'some_metadata') }
+ let_it_be(:candidate) { metadata.candidate }
+
+ it 'is unique within candidate' do
+ expect do
+ candidate.metadata.create!(name: 'some_metadata', value: 'blah')
+ end.to raise_error.with_message(/Name 'some_metadata' already taken/)
+ end
+ end
+end
diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb
index b35496363fe..9ce411191f0 100644
--- a/spec/models/ml/candidate_spec.rb
+++ b/spec/models/ml/candidate_spec.rb
@@ -5,11 +5,18 @@ require 'spec_helper'
RSpec.describe Ml::Candidate, factory_default: :keep do
let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params) }
+ let(:project) { candidate.experiment.project }
+
describe 'associations' do
it { is_expected.to belong_to(:experiment) }
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:params) }
it { is_expected.to have_many(:metrics) }
+ it { is_expected.to have_many(:metadata) }
+ end
+
+ describe 'default values' do
+ it { expect(described_class.new.iid).to be_present }
end
describe '.artifact_root' do
@@ -18,8 +25,38 @@ RSpec.describe Ml::Candidate, factory_default: :keep do
it { is_expected.to eq("/ml_candidate_#{candidate.iid}/-/") }
end
- describe 'default values' do
- it { expect(described_class.new.iid).to be_present }
+ describe '.package_name' do
+ subject { candidate.package_name }
+
+ it { is_expected.to eq("ml_candidate_#{candidate.iid}") }
+ end
+
+ describe '.package_version' do
+ subject { candidate.package_version }
+
+ it { is_expected.to eq('-') }
+ end
+
+ describe '.artifact' do
+ subject { candidate.artifact }
+
+ context 'when has logged artifacts' do
+ let(:package) do
+ create(:generic_package, name: candidate.package_name, version: candidate.package_version, project: project)
+ end
+
+ it 'returns the package' do
+ package
+
+ is_expected.to eq(package)
+ end
+ end
+
+ context 'when does not have logged artifacts' do
+ let(:tested_candidate) { create(:ml_candidates, :with_metrics_and_params) }
+
+ it { is_expected.to be_nil }
+ end
end
describe '#by_project_id_and_iid' do
diff --git a/spec/models/ml/experiment_metadata_spec.rb b/spec/models/ml/experiment_metadata_spec.rb
new file mode 100644
index 00000000000..e989d495a1c
--- /dev/null
+++ b/spec/models/ml/experiment_metadata_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::ExperimentMetadata, feature_category: :mlops do
+ describe 'associations' do
+ it { is_expected.to belong_to(:experiment) }
+ end
+
+ describe 'uniqueness of name' do
+ let_it_be(:metadata) { create(:ml_experiment_metadata, name: 'some_metadata') }
+ let_it_be(:experiment) { metadata.experiment }
+
+ it 'is unique within experiment' do
+ expect do
+ experiment.metadata.create!(name: 'some_metadata', value: 'blah')
+ end.to raise_error.with_message(/Name 'some_metadata' already taken/)
+ end
+ end
+end
diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb
index 789bb3aa88a..52e9f9217f5 100644
--- a/spec/models/ml/experiment_spec.rb
+++ b/spec/models/ml/experiment_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe Ml::Experiment do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:candidates) }
+ it { is_expected.to have_many(:metadata) }
end
describe '#by_project_id_and_iid' do
diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb
index 17c49e13c85..e06a6a30f9a 100644
--- a/spec/models/namespace_setting_spec.rb
+++ b/spec/models/namespace_setting_spec.rb
@@ -178,6 +178,63 @@ RSpec.describe NamespaceSetting, type: :model do
end
end
+ describe '#runner_registration_enabled?' do
+ context 'when not a subgroup' do
+ let_it_be(:settings) { create(:namespace_settings) }
+ let_it_be(:group) { create(:group, namespace_settings: settings) }
+
+ before do
+ group.update!(runner_registration_enabled: runner_registration_enabled)
+ end
+
+ context 'when :runner_registration_enabled is false' do
+ let(:runner_registration_enabled) { false }
+
+ it 'returns false' do
+ expect(group.runner_registration_enabled?).to be_falsey
+ end
+
+ it 'does not query the db' do
+ expect { group.runner_registration_enabled? }.not_to exceed_query_limit(0)
+ end
+ end
+
+ context 'when :runner_registration_enabled is true' do
+ let(:runner_registration_enabled) { true }
+
+ it 'returns true' do
+ expect(group.runner_registration_enabled?).to be_truthy
+ end
+ end
+ end
+
+ context 'when a group has parent groups' do
+ let_it_be(:grandparent) { create(:group) }
+ let_it_be(:parent) { create(:group, parent: grandparent) }
+ let_it_be(:group) { create(:group, parent: parent) }
+
+ before do
+ grandparent.update!(runner_registration_enabled: runner_registration_enabled)
+ end
+
+ context 'when a parent group has runner registration disabled' do
+ let(:runner_registration_enabled) { false }
+
+ it 'returns false' do
+ expect(group.runner_registration_enabled?).to be_falsey
+ end
+ end
+
+ context 'when all parent groups have runner registration enabled' do
+ let(:runner_registration_enabled) { true }
+
+ it 'returns true' do
+ expect(group.runner_registration_enabled?).to be_truthy
+ end
+ end
+ end
+ end
+
describe '#delayed_project_removal' do
it_behaves_like 'a cascading namespace setting boolean attribute', settings_attribute_name: :delayed_project_removal
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 0516d446945..80721e11049 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -34,6 +34,7 @@ RSpec.describe Namespace do
it { is_expected.to have_many :member_roles }
it { is_expected.to have_one :cluster_enabled_grant }
it { is_expected.to have_many(:work_items) }
+ it { is_expected.to have_many :achievements }
it do
is_expected.to have_one(:ci_cd_settings).class_name('NamespaceCiCdSetting').inverse_of(:namespace).autosave(true)
@@ -1895,6 +1896,30 @@ RSpec.describe Namespace do
end
end
+ describe '#bot_user_namespace?' do
+ subject { namespace.bot_user_namespace? }
+
+ context 'when owner is a bot user user' do
+ let(:user) { create(:user, :project_bot) }
+ let(:namespace) { user.namespace }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when owner is a non-bot user' do
+ let(:user) { create(:user) }
+ let(:namespace) { user.namespace }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when type is a group' do
+ let(:namespace) { create(:group) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
describe '#aggregation_scheduled?' do
let(:namespace) { create(:namespace) }
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 7c71080d63e..328d3ba7dda 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -579,6 +579,7 @@ RSpec.describe Note do
expect(commit_note.confidential?).to be_falsy
end
end
+
context 'when note is confidential' do
it 'is true even when a noteable is not confidential' do
issue = create(:issue, confidential: false)
diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb
index 068166ebb0d..2275bea4c7f 100644
--- a/spec/models/notification_recipient_spec.rb
+++ b/spec/models/notification_recipient_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe NotificationRecipient do
+RSpec.describe NotificationRecipient, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:target) { create(:issue, project: project) }
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index c665f738ead..a244ed34e54 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -104,15 +104,9 @@ RSpec.describe Packages::PackageFile, type: :model do
let_it_be(:package, reload: true) { create(:package) }
context 'when the package file has an explicit size' do
- it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:package_file, :jar, package: package, size: 42) }
- end
- end
+ subject { build(:package_file, :jar, package: package, size: 42) }
- context 'when the package file does not have a size' do
- it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:package_file, package: package, size: nil) }
- end
+ it_behaves_like 'UpdateProjectStatistics', :packages_size
end
end
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 241c585099c..d6f71f2401c 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -708,12 +708,14 @@ RSpec.describe Packages::Package, type: :model do
describe '#destroy' do
let(:package) { create(:npm_package) }
let(:package_file) { package.package_files.first }
- let(:project_statistics) { ProjectStatistics.for_project_ids(package.project.id).first }
+ let(:project_statistics) { package.project.statistics }
- it 'affects project statistics' do
- expect { package.destroy! }
- .to change { project_statistics.reload.packages_size }
- .from(package_file.size).to(0)
+ subject(:destroy!) { package.destroy! }
+
+ it 'updates the project statistics' do
+ expect(project_statistics).to receive(:increment_counter).with(:packages_size, -package_file.size)
+
+ destroy!
end
end
diff --git a/spec/models/packages/rpm/repository_file_spec.rb b/spec/models/packages/rpm/repository_file_spec.rb
index 34347793dd8..1147fd66ac6 100644
--- a/spec/models/packages/rpm/repository_file_spec.rb
+++ b/spec/models/packages/rpm/repository_file_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Rpm::RepositoryFile, type: :model do
+RSpec.describe Packages::Rpm::RepositoryFile, type: :model, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
let_it_be(:repository_file) { create(:rpm_repository_file) }
@@ -16,6 +16,32 @@ RSpec.describe Packages::Rpm::RepositoryFile, type: :model do
it { is_expected.to validate_presence_of(:project) }
end
+ describe '.has_oversized_filelists?' do
+ let_it_be(:filelists) { create(:rpm_repository_file, :filelists, size: 21.megabytes) }
+
+ subject { described_class.has_oversized_filelists?(project_id: filelists.project_id) }
+
+ context 'when has oversized filelists' do
+ it { expect(subject).to be true }
+ end
+
+ context 'when filelists.xml is not oversized' do
+ before do
+ filelists.update!(size: 19.megabytes)
+ end
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when there is no filelists.xml' do
+ before do
+ filelists.destroy!
+ end
+
+ it { expect(subject).to be_falsey }
+ end
+ end
+
context 'when updating project statistics' do
context 'when the package file has an explicit size' do
it_behaves_like 'UpdateProjectStatistics' do
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index 2d7ee8ba3be..6f684eceaec 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -74,6 +74,12 @@ RSpec.describe Pages::LookupPath 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)
diff --git a/spec/models/pages_deployment_spec.rb b/spec/models/pages_deployment_spec.rb
index a27d836e2c2..268c5006a88 100644
--- a/spec/models/pages_deployment_spec.rb
+++ b/spec/models/pages_deployment_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PagesDeployment do
+RSpec.describe PagesDeployment, feature_category: :pages do
let_it_be(:project) { create(:project) }
describe 'associations' do
diff --git a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
index ee2407f21b6..21b16bdeb17 100644
--- a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
+++ b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
@@ -229,83 +229,37 @@ RSpec.describe PerformanceMonitoring::PrometheusDashboard do
allow(Gitlab::Metrics::Dashboard::Finder).to receive(:find_raw).with(project, dashboard_path: path).and_call_original
end
- context 'metrics_dashboard_exhaustive_validations is on' do
- before do
- stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
- end
-
- context 'when schema is valid' do
- let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) }
+ context 'when schema is valid' do
+ let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) }
- it 'returns empty array' do
- expect(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).with(dashboard_schema, dashboard_path: path, project: project).and_return([])
+ it 'returns empty array' do
+ expect(described_class).to receive(:from_json).with(dashboard_schema)
- expect(schema_validation_warnings).to eq []
- end
- end
-
- context 'when schema is invalid' do
- let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) }
-
- it 'returns array with errors messages' do
- error = ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new
-
- expect(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).with(dashboard_schema, dashboard_path: path, project: project).and_return([error])
-
- expect(schema_validation_warnings).to eq [error.message]
- end
- end
-
- context 'when YAML has wrong syntax' do
- let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) }
-
- subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings }
-
- it 'returns array with errors messages' do
- expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors)
-
- expect(schema_validation_warnings).to eq ['Invalid yaml']
- end
+ expect(schema_validation_warnings).to eq []
end
end
- context 'metrics_dashboard_exhaustive_validations is off' do
- before do
- stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
- end
-
- context 'when schema is valid' do
- let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) }
-
- it 'returns empty array' do
- expect(described_class).to receive(:from_json).with(dashboard_schema)
-
- expect(schema_validation_warnings).to eq []
- end
- end
-
- context 'when schema is invalid' do
- let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) }
+ context 'when schema is invalid' do
+ let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) }
- it 'returns array with errors messages' do
- instance = described_class.new
- instance.errors.add(:test, 'test error')
+ it 'returns array with errors messages' do
+ instance = described_class.new
+ instance.errors.add(:test, 'test error')
- expect(described_class).to receive(:from_json).and_raise(ActiveModel::ValidationError.new(instance))
- expect(described_class.new.schema_validation_warnings).to eq ['test: test error']
- end
+ expect(described_class).to receive(:from_json).and_raise(ActiveModel::ValidationError.new(instance))
+ expect(described_class.new.schema_validation_warnings).to eq ['test: test error']
end
+ end
- context 'when YAML has wrong syntax' do
- let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) }
+ context 'when YAML has wrong syntax' do
+ let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) }
- subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings }
+ subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings }
- it 'returns array with errors messages' do
- expect(described_class).not_to receive(:from_json)
+ it 'returns array with errors messages' do
+ expect(described_class).not_to receive(:from_json)
- expect(schema_validation_warnings).to eq ['Invalid yaml']
- end
+ expect(schema_validation_warnings).to eq ['Invalid yaml']
end
end
end
diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb
index f9c458b2c80..d4e550657c8 100644
--- a/spec/models/plan_limits_spec.rb
+++ b/spec/models/plan_limits_spec.rb
@@ -200,6 +200,7 @@ RSpec.describe PlanLimits do
ci_max_artifact_size_cluster_applications
ci_max_artifact_size_secret_detection
ci_max_artifact_size_requirements
+ ci_max_artifact_size_requirements_v2
ci_max_artifact_size_coverage_fuzzing
ci_max_artifact_size_api_fuzzing
]
diff --git a/spec/models/programming_language_spec.rb b/spec/models/programming_language_spec.rb
index b202c10e30b..403cd77c707 100644
--- a/spec/models/programming_language_spec.rb
+++ b/spec/models/programming_language_spec.rb
@@ -3,6 +3,10 @@
require 'spec_helper'
RSpec.describe ProgrammingLanguage do
+ let_it_be(:ruby) { create(:programming_language, name: 'Ruby') }
+ let_it_be(:python) { create(:programming_language, name: 'Python') }
+ let_it_be(:swift) { create(:programming_language, name: 'Swift') }
+
it { is_expected.to respond_to(:name) }
it { is_expected.to respond_to(:color) }
@@ -12,10 +16,6 @@ RSpec.describe ProgrammingLanguage do
it { is_expected.not_to allow_value("#0z0000").for(:color) }
describe '.with_name_case_insensitive scope' do
- let_it_be(:ruby) { create(:programming_language, name: 'Ruby') }
- let_it_be(:python) { create(:programming_language, name: 'Python') }
- let_it_be(:swift) { create(:programming_language, name: 'Swift') }
-
it 'accepts a single name parameter' do
expect(described_class.with_name_case_insensitive('swift')).to(
contain_exactly(swift)
@@ -28,4 +28,17 @@ RSpec.describe ProgrammingLanguage do
)
end
end
+
+ describe '.most_popular' do
+ before do
+ create_list(:repository_language, 3, programming_language_id: ruby.id)
+ create(:repository_language, programming_language_id: python.id)
+ create_list(:repository_language, 2, programming_language_id: swift.id)
+ ApplicationRecord.connection.execute('analyze repository_languages')
+ end
+
+ it 'returns the most popular programming languages' do
+ expect(described_class.most_popular(2)).to(contain_exactly(ruby, swift))
+ end
+ end
end
diff --git a/spec/models/project_export_job_spec.rb b/spec/models/project_export_job_spec.rb
index 653d4d2df27..01b0aaff0ff 100644
--- a/spec/models/project_export_job_spec.rb
+++ b/spec/models/project_export_job_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectExportJob, type: :model do
+RSpec.describe ProjectExportJob, feature_category: :importers, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:relation_exports) }
@@ -13,4 +13,54 @@ RSpec.describe ProjectExportJob, type: :model do
it { is_expected.to validate_presence_of(:jid) }
it { is_expected.to validate_presence_of(:status) }
end
+
+ context 'when pruning expired jobs' do
+ let_it_be(:old_job_1) { create(:project_export_job, updated_at: 37.months.ago) }
+ let_it_be(:old_job_2) { create(:project_export_job, updated_at: 12.months.ago) }
+ let_it_be(:old_job_3) { create(:project_export_job, updated_at: 8.days.ago) }
+ let_it_be(:fresh_job_1) { create(:project_export_job, updated_at: 1.day.ago) }
+ let_it_be(:fresh_job_2) { create(:project_export_job, updated_at: 2.days.ago) }
+ let_it_be(:fresh_job_3) { create(:project_export_job, updated_at: 6.days.ago) }
+
+ let_it_be(:old_relation_export_1) { create(:project_relation_export, project_export_job_id: old_job_1.id) }
+ let_it_be(:old_relation_export_2) { create(:project_relation_export, project_export_job_id: old_job_2.id) }
+ let_it_be(:old_relation_export_3) { create(:project_relation_export, project_export_job_id: old_job_3.id) }
+ let_it_be(:fresh_relation_export_1) { create(:project_relation_export, project_export_job_id: fresh_job_1.id) }
+
+ let_it_be(:old_upload_1) { create(:relation_export_upload, project_relation_export_id: old_relation_export_1.id) }
+ let_it_be(:old_upload_2) { create(:relation_export_upload, project_relation_export_id: old_relation_export_2.id) }
+ let_it_be(:old_upload_3) { create(:relation_export_upload, project_relation_export_id: old_relation_export_3.id) }
+ let_it_be(:fresh_upload_1) do
+ create(
+ :relation_export_upload,
+ project_relation_export_id: fresh_relation_export_1.id
+ )
+ end
+
+ it 'prunes jobs and associations older than 7 days' do
+ expect { described_class.prune_expired_jobs }.to change { described_class.count }.by(-3)
+
+ expect(described_class.find_by(id: old_job_1.id)).to be_nil
+ expect(described_class.find_by(id: old_job_2.id)).to be_nil
+ expect(described_class.find_by(id: old_job_3.id)).to be_nil
+
+ expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_1.id)).to be_nil
+ expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_2.id)).to be_nil
+ expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_3.id)).to be_nil
+
+ expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_1.id)).to be_nil
+ expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_2.id)).to be_nil
+ expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_3.id)).to be_nil
+ end
+
+ it 'does not delete associated records for jobs younger than 7 days' do
+ described_class.prune_expired_jobs
+
+ expect(fresh_job_1.reload).to be_present
+ expect(fresh_job_2.reload).to be_present
+ expect(fresh_job_3.reload).to be_present
+ expect(fresh_relation_export_1.reload).to be_present
+ expect(fresh_upload_1.reload).to be_present
+ end
+ end
end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index dae0f84eda3..fb6aaffdf22 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -291,6 +291,7 @@ RSpec.describe ProjectFeature do
end
end
end
+
# rubocop:disable Gitlab/FeatureAvailableUsage
describe '#feature_available?' do
let(:features) { ProjectFeature::FEATURES }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 1cae03ae2ae..f33001b9c5b 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -21,7 +21,6 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to belong_to(:creator).class_name('User') }
it { is_expected.to belong_to(:pool_repository) }
it { is_expected.to have_many(:users) }
- it { is_expected.to have_many(:integrations) }
it { is_expected.to have_many(:events) }
it { is_expected.to have_many(:merge_requests) }
it { is_expected.to have_many(:merge_request_metrics).class_name('MergeRequest::Metrics') }
@@ -58,7 +57,6 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_one(:pipelines_email_integration) }
it { is_expected.to have_one(:irker_integration) }
it { is_expected.to have_one(:pivotaltracker_integration) }
- it { is_expected.to have_one(:flowdock_integration) }
it { is_expected.to have_one(:assembla_integration) }
it { is_expected.to have_one(:slack_slash_commands_integration) }
it { is_expected.to have_one(:mattermost_slash_commands_integration) }
@@ -151,6 +149,20 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_many(:project_callouts).class_name('Users::ProjectCallout').with_foreign_key(:project_id) }
it { is_expected.to have_many(:pipeline_metadata).class_name('Ci::PipelineMetadata') }
it { is_expected.to have_many(:incident_management_timeline_event_tags).class_name('IncidentManagement::TimelineEventTag') }
+ it { is_expected.to have_many(:integrations) }
+ it { is_expected.to have_many(:push_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:tag_push_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:issue_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:confidential_issue_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:merge_request_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:note_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:confidential_note_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:job_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:archive_trace_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:pipeline_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:wiki_page_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:deployment_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:alert_hooks_integrations).class_name('Integration') }
# GitLab Pages
it { is_expected.to have_many(:pages_domains) }
@@ -356,6 +368,33 @@ RSpec.describe Project, factory_default: :keep do
subject.ci_pipelines
end
end
+
+ context 'order of the `has_many :notes` association' do
+ let(:associations_having_dependent_destroy) do
+ described_class.reflect_on_all_associations(:has_many).select do |assoc|
+ assoc.options[:dependent] == :destroy
+ end
+ end
+
+ let(:associations_having_dependent_destroy_with_issuable_included) do
+ associations_having_dependent_destroy.select do |association|
+ association.klass.include?(Issuable)
+ end
+ end
+
+ it 'has `has_many :notes` as the first association among all the other associations that'\
+ 'includes the `Issuable` module' do
+ names_of_associations_having_dependent_destroy = associations_having_dependent_destroy.map(&:name)
+ index_of_has_many_notes_association = names_of_associations_having_dependent_destroy.find_index(:notes)
+
+ associations_having_dependent_destroy_with_issuable_included.each do |issuable_included_association|
+ index_of_issuable_included_association =
+ names_of_associations_having_dependent_destroy.find_index(issuable_included_association.name)
+
+ expect(index_of_has_many_notes_association).to be < index_of_issuable_included_association
+ end
+ end
+ end
end
describe 'modules' do
@@ -5759,6 +5798,32 @@ RSpec.describe Project, factory_default: :keep do
integration.project.execute_integrations(anything, :merge_request_hooks)
end
+
+ it 'does not trigger extra queries when called multiple times' do
+ integration.project.execute_integrations({}, :push_hooks)
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ integration.project.execute_integrations({}, :push_hooks)
+ end
+
+ expect(recorder.count).to be_zero
+ end
+
+ context 'with cache_project_integrations disabled' do
+ before do
+ stub_feature_flags(cache_project_integrations: false)
+ end
+
+ it 'triggers extra queries when called multiple times' do
+ integration.project.execute_integrations({}, :push_hooks)
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ integration.project.execute_integrations({}, :push_hooks)
+ end
+
+ expect(recorder.count).not_to be_zero
+ end
+ end
end
describe '#has_active_hooks?' do
@@ -8156,6 +8221,16 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#work_items_mvc_feature_flag_enabled?' do
+ let_it_be(:group_project) { create(:project, :in_subgroup) }
+
+ it_behaves_like 'checks parent group feature flag' do
+ let(:feature_flag_method) { :work_items_mvc_feature_flag_enabled? }
+ let(:feature_flag) { :work_items_mvc }
+ let(:subject_project) { group_project }
+ end
+ end
+
describe '#work_items_mvc_2_feature_flag_enabled?' do
let_it_be(:group_project) { create(:project, :in_subgroup) }
@@ -8370,6 +8445,105 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to be(false) }
end
+ describe '.cascading_with_parent_namespace' do
+ let_it_be_with_reload(:group) { create(:group, :with_root_storage_statistics) }
+ let_it_be_with_reload(:subgroup) { create(:group, parent: group) }
+ let_it_be_with_reload(:project) { create(:project, group: subgroup) }
+ let_it_be_with_reload(:project_without_group) { create(:project) }
+
+ shared_examples 'cascading settings' do |attribute|
+ it 'return self value when no parent' do
+ expect(project_without_group.group).to be_nil
+
+ project_without_group.update!(attribute => true)
+ expect(project_without_group.public_send("#{attribute}?", inherit_group_setting: true)).to be_truthy
+ expect(project_without_group.public_send("#{attribute}_locked?")).to be_falsey
+
+ project_without_group.update!(attribute => false)
+ expect(project_without_group.public_send("#{attribute}?", inherit_group_setting: true)).to be_falsey
+ expect(project_without_group.public_send("#{attribute}_locked?")).to be_falsey
+ end
+
+ it 'return self value when unlocked' do
+ subgroup.namespace_settings.update!(attribute => false)
+ group.namespace_settings.update!(attribute => false)
+
+ project.update!(attribute => true)
+ expect(project.public_send("#{attribute}?", inherit_group_setting: true)).to be_truthy
+ expect(project.public_send("#{attribute}_locked?")).to be_falsey
+
+ project.update!(attribute => false)
+ expect(project.public_send("#{attribute}?", inherit_group_setting: true)).to be_falsey
+ expect(project.public_send("#{attribute}_locked?")).to be_falsey
+ end
+
+ it 'still return self value when locked subgroup' do
+ subgroup.namespace_settings.update!(attribute => true)
+ group.namespace_settings.update!(attribute => false)
+
+ project.update!(attribute => true)
+ expect(project.public_send("#{attribute}?", inherit_group_setting: true)).to be_truthy
+ expect(project.public_send("#{attribute}_locked?")).to be_falsey
+
+ project.update!(attribute => false)
+ expect(project.public_send("#{attribute}?", inherit_group_setting: true)).to be_falsey
+ expect(project.public_send("#{attribute}_locked?")).to be_falsey
+ end
+
+ it 'still return unlocked value when locked group' do
+ subgroup.namespace_settings.update!(attribute => false)
+ group.namespace_settings.update!(attribute => true)
+
+ project.update!(attribute => true)
+ expect(project.public_send("#{attribute}?", inherit_group_setting: true)).to be_truthy
+ expect(project.public_send("#{attribute}_locked?")).to be_falsey
+
+ project.update!(attribute => false)
+ expect(project.public_send("#{attribute}?", inherit_group_setting: true)).to be_falsey
+ expect(project.public_send("#{attribute}_locked?")).to be_falsey
+ end
+ end
+
+ it_behaves_like 'cascading settings', :only_allow_merge_if_pipeline_succeeds
+ it_behaves_like 'cascading settings', :allow_merge_on_skipped_pipeline
+ it_behaves_like 'cascading settings', :only_allow_merge_if_all_discussions_are_resolved
+ end
+
+ describe '#archived' do
+ it { expect(subject.archived).to be_falsey }
+ it { expect(described_class.new(archived: true).archived).to be_truthy }
+ end
+
+ describe '#resolve_outdated_diff_discussions' do
+ it { expect(subject.resolve_outdated_diff_discussions).to be_falsey }
+
+ context 'when set explicitly' do
+ subject { described_class.new(resolve_outdated_diff_discussions: true) }
+
+ it { expect(subject.resolve_outdated_diff_discussions).to be_truthy }
+ end
+ end
+
+ describe '#only_allow_merge_if_all_discussions_are_resolved' do
+ it { expect(subject.only_allow_merge_if_all_discussions_are_resolved).to be_falsey }
+
+ context 'when set explicitly' do
+ subject { described_class.new(only_allow_merge_if_all_discussions_are_resolved: true) }
+
+ it { expect(subject.only_allow_merge_if_all_discussions_are_resolved).to be_truthy }
+ end
+ end
+
+ describe '#remove_source_branch_after_merge' do
+ it { expect(subject.remove_source_branch_after_merge).to be_truthy }
+
+ context 'when set explicitly' do
+ subject { described_class.new(remove_source_branch_after_merge: false) }
+
+ it { expect(subject.remove_source_branch_after_merge).to be_falsey }
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 9de31ea66e4..a6e2bcf1525 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -467,6 +467,13 @@ RSpec.describe ProjectStatistics do
.to change { statistics.reload.storage_size }
.by(20)
end
+
+ it 'schedules a namespace aggregation worker' do
+ expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async)
+ .with(statistics.project.namespace.id)
+
+ described_class.increment_statistic(project, stat, 20)
+ end
end
shared_examples 'a statistic that increases storage_size asynchronously' do
@@ -474,7 +481,8 @@ RSpec.describe ProjectStatistics do
described_class.increment_statistic(project, stat, 13)
Gitlab::Redis::SharedState.with do |redis|
- increment = redis.get(statistics.counter_key(stat))
+ key = statistics.counter(stat).key
+ increment = redis.get(key)
expect(increment.to_i).to eq(13)
end
end
@@ -482,7 +490,7 @@ RSpec.describe ProjectStatistics do
it 'schedules a worker to update the statistic and storage_size async', :sidekiq_inline do
expect(FlushCounterIncrementsWorker)
.to receive(:perform_in)
- .with(CounterAttribute::WORKER_DELAY, described_class.name, statistics.id, stat)
+ .with(Gitlab::Counters::BufferedCounter::WORKER_DELAY, described_class.name, statistics.id, stat)
.and_call_original
expect { described_class.increment_statistic(project, stat, 20) }
@@ -506,20 +514,20 @@ RSpec.describe ProjectStatistics do
context 'when adjusting :packages_size' do
let(:stat) { :packages_size }
- it_behaves_like 'a statistic that increases storage_size'
+ it_behaves_like 'a statistic that increases storage_size asynchronously'
end
context 'when the amount is 0' do
it 'does not execute a query' do
project
- expect { described_class.increment_statistic(project.id, :build_artifacts_size, 0) }
+ expect { described_class.increment_statistic(project, :build_artifacts_size, 0) }
.not_to exceed_query_limit(0)
end
end
context 'when using an invalid column' do
it 'raises an error' do
- expect { described_class.increment_statistic(project.id, :id, 13) }
+ expect { described_class.increment_statistic(project, :id, 13) }
.to raise_error(ArgumentError, "Cannot increment attribute: id")
end
end
diff --git a/spec/models/projects/build_artifacts_size_refresh_spec.rb b/spec/models/projects/build_artifacts_size_refresh_spec.rb
index 21cd8e0b9d4..caff06262d9 100644
--- a/spec/models/projects/build_artifacts_size_refresh_spec.rb
+++ b/spec/models/projects/build_artifacts_size_refresh_spec.rb
@@ -67,6 +67,8 @@ RSpec.describe Projects::BuildArtifactsSizeRefresh, type: :model do
let!(:last_job_artifact_id_on_refresh_start) { create(:ci_job_artifact, project: refresh.project) }
+ let(:statistics) { refresh.project.statistics }
+
before do
stats = create(:project_statistics, project: refresh.project, build_artifacts_size: 120)
stats.increment_counter(:build_artifacts_size, 30)
@@ -89,11 +91,11 @@ RSpec.describe Projects::BuildArtifactsSizeRefresh, type: :model do
end
it 'resets the build artifacts size stats' do
- expect { refresh.process! }.to change { refresh.project.statistics.build_artifacts_size }.to(0)
+ expect { refresh.process! }.to change { statistics.build_artifacts_size }.to(0)
end
it 'resets the counter attribute to zero' do
- expect { refresh.process! }.to change { refresh.project.statistics.get_counter_value(:build_artifacts_size) }.to(0)
+ expect { refresh.process! }.to change { statistics.counter(:build_artifacts_size).get }.to(0)
end
end
diff --git a/spec/models/projects/forks/divergence_counts_spec.rb b/spec/models/projects/forks/divergence_counts_spec.rb
new file mode 100644
index 00000000000..fd69cc0f3e7
--- /dev/null
+++ b/spec/models/projects/forks/divergence_counts_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Forks::DivergenceCounts, feature_category: :source_code_management do
+ include ProjectForksHelper
+
+ let_it_be(:user) { create(:user) }
+
+ describe '#counts', :use_clean_rails_redis_caching do
+ let(:source_repo) { create(:project, :repository, :public).repository }
+ let(:fork_repo) { fork_project(source_repo.project, user, { repository: true }).repository }
+ let(:fork_branch) { 'fork-branch' }
+ let(:cache_key) { ['project_forks', fork_repo.project.id, fork_branch, 'divergence_counts'] }
+
+ def expect_cached_counts(value)
+ counts = described_class.new(fork_repo.project, fork_branch).counts
+
+ ahead, behind = value
+ expect(counts).to eq({ ahead: ahead, behind: behind })
+
+ cached_value = [source_repo.commit.sha, fork_repo.commit(fork_branch).sha, value]
+ expect(Rails.cache.read(cache_key)).to eq(cached_value)
+ end
+
+ it 'shows how far behind/ahead a fork is from the upstream' do
+ fork_repo.create_branch(fork_branch)
+
+ expect_cached_counts([0, 0])
+
+ fork_repo.commit_files(
+ user,
+ branch_name: fork_branch, message: 'Committing something',
+ actions: [{ action: :create, file_path: 'encoding/CHANGELOG', content: 'New file' }]
+ )
+
+ expect_cached_counts([1, 0])
+
+ fork_repo.commit_files(
+ user,
+ branch_name: fork_branch, message: 'Committing something else',
+ actions: [{ action: :create, file_path: 'encoding/ONE-MORE-CHANGELOG', content: 'One more new file' }]
+ )
+
+ expect_cached_counts([2, 0])
+
+ source_repo.commit_files(
+ user,
+ branch_name: source_repo.root_ref, message: 'Commit to root ref',
+ actions: [{ action: :create, file_path: 'encoding/CHANGELOG', content: 'One more' }]
+ )
+
+ expect_cached_counts([2, 1])
+
+ source_repo.commit_files(
+ user,
+ branch_name: source_repo.root_ref, message: 'Another commit to root ref',
+ actions: [{ action: :create, file_path: 'encoding/NEW-CHANGELOG', content: 'One more time' }]
+ )
+
+ expect_cached_counts([2, 2])
+
+ # When the fork is too far ahead
+ stub_const("#{described_class}::LATEST_COMMITS_COUNT", 1)
+ fork_repo.commit_files(
+ user,
+ branch_name: fork_branch, message: 'Another commit to fork',
+ actions: [{ action: :create, file_path: 'encoding/TOO-NEW-CHANGELOG', content: 'New file' }]
+ )
+
+ expect_cached_counts(nil)
+ end
+
+ context 'when counts calculated from a branch that exists upstream' do
+ let(:fork_branch) { 'feature' }
+
+ it 'compares the fork branch to upstream default branch' do
+ # The branch itself diverges from the upstream default branch
+ expect_cached_counts([1, 29])
+
+ source_repo.commit_files(
+ user,
+ branch_name: source_repo.root_ref, message: 'Commit to root ref',
+ actions: [{ action: :create, file_path: 'encoding/CHANGELOG', content: 'New file' }]
+ )
+
+ fork_repo.commit_files(
+ user,
+ branch_name: fork_branch, message: 'Committing to feature branch',
+ actions: [{ action: :create, file_path: 'encoding/FEATURE-BRANCH', content: 'New file' }]
+ )
+
+ # It takes into account diverged commits from upstream AND from fork
+ expect_cached_counts([2, 30])
+ end
+ end
+ end
+end
diff --git a/spec/models/release_highlight_spec.rb b/spec/models/release_highlight_spec.rb
index 3555dfba769..4148452f849 100644
--- a/spec/models/release_highlight_spec.rb
+++ b/spec/models/release_highlight_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
- let(:fixture_dir_glob) { Dir.glob(File.join(Rails.root, 'spec', 'fixtures', 'whats_new', '*.yml')).grep(/\d*\_(\d*\_\d*)\.yml$/) }
+RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache, feature_category: :release_orchestration do
+ let(:fixture_dir_glob) { Dir.glob(File.join(Rails.root, 'spec', 'fixtures', 'whats_new', '*.yml')).grep(/\d*_(\d*_\d*)\.yml$/) }
before do
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index c17e180f282..969a279dd52 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -5,7 +5,9 @@ require 'spec_helper'
RSpec.describe Repository do
include RepoHelpers
- TestBlob = Struct.new(:path)
+ before do
+ stub_const('TestBlob', Struct.new(:path))
+ end
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
@@ -164,11 +166,11 @@ RSpec.describe Repository do
repository.add_tag(user, annotated_tag_name, 'a48e4fc218069f68ef2e769dd8dfea3991362175', 'test tag message\n')
end
- it { is_expected.to eq(['v1.0.0', 'v1.1.0', annotated_tag_name]) }
-
after do
repository.rm_tag(user, annotated_tag_name)
end
+
+ it { is_expected.to eq(['v1.0.0', 'v1.1.0', annotated_tag_name]) }
end
end
@@ -2224,32 +2226,34 @@ RSpec.describe Repository do
describe '#after_change_head' do
it 'flushes the method caches' do
- expect(repository).to receive(:expire_method_caches).with([
- :size,
- :commit_count,
- :readme_path,
- :contribution_guide,
- :changelog,
- :license_blob,
- :license_licensee,
- :license_gitaly,
- :gitignore,
- :gitlab_ci_yml,
- :branch_names,
- :tag_names,
- :branch_count,
- :tag_count,
- :avatar,
- :exists?,
- :root_ref,
- :merged_branch_names,
- :has_visible_content?,
- :issue_template_names_hash,
- :merge_request_template_names_hash,
- :user_defined_metrics_dashboard_paths,
- :xcode_project?,
- :has_ambiguous_refs?
- ])
+ expect(repository).to receive(:expire_method_caches).with(
+ [
+ :size,
+ :commit_count,
+ :readme_path,
+ :contribution_guide,
+ :changelog,
+ :license_blob,
+ :license_licensee,
+ :license_gitaly,
+ :gitignore,
+ :gitlab_ci_yml,
+ :branch_names,
+ :tag_names,
+ :branch_count,
+ :tag_count,
+ :avatar,
+ :exists?,
+ :root_ref,
+ :merged_branch_names,
+ :has_visible_content?,
+ :issue_template_names_hash,
+ :merge_request_template_names_hash,
+ :user_defined_metrics_dashboard_paths,
+ :xcode_project?,
+ :has_ambiguous_refs?
+ ]
+ )
repository.after_change_head
end
diff --git a/spec/models/service_desk_setting_spec.rb b/spec/models/service_desk_setting_spec.rb
index f99ac84175c..c1ec35732b8 100644
--- a/spec/models/service_desk_setting_spec.rb
+++ b/spec/models/service_desk_setting_spec.rb
@@ -39,11 +39,16 @@ RSpec.describe ServiceDeskSetting do
let_it_be(:project1) { create(:project, name: 'test-one', group: group) }
let_it_be(:project2) { create(:project, name: 'one', group: subgroup) }
let_it_be(:project_key) { 'key' }
-
- before_all do
+ let!(:setting) do
create(:service_desk_setting, project: project1, project_key: project_key)
end
+ context 'when project_key exists' do
+ it 'is valid' do
+ expect(setting).to be_valid
+ end
+ end
+
context 'when project_key is unique for every project slug' do
it 'does not add error' do
settings = build(:service_desk_setting, project: project2, project_key: 'otherkey')
diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb
index 655cfad57c9..050f99fd4d5 100644
--- a/spec/models/snippet_repository_spec.rb
+++ b/spec/models/snippet_repository_spec.rb
@@ -38,6 +38,9 @@ RSpec.describe SnippetRepository do
let(:update_file) { { previous_path: 'README', file_path: 'README', content: 'bar' } }
let(:data) { [new_file, move_file, update_file] }
+ let_it_be(:unnamed_snippet) { { file_path: '', content: 'dummy', action: :create } }
+ let_it_be(:named_snippet) { { file_path: 'fee.txt', content: 'bar', action: :create } }
+
it 'returns nil when files argument is empty' do
expect(snippet.repository).not_to receive(:commit_files)
@@ -210,9 +213,6 @@ RSpec.describe SnippetRepository do
end
end
- let_it_be(:named_snippet) { { file_path: 'fee.txt', content: 'bar', action: :create } }
- let_it_be(:unnamed_snippet) { { file_path: '', content: 'dummy', action: :create } }
-
context 'when existing file has a default name' do
let(:default_name) { 'snippetfile1.txt' }
let(:new_file) { { file_path: '', content: 'bar' } }
diff --git a/spec/models/state_note_spec.rb b/spec/models/state_note_spec.rb
index e91150695b0..0afdf6bbcb9 100644
--- a/spec/models/state_note_spec.rb
+++ b/spec/models/state_note_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe StateNote do
it 'contains the expected values' do
expect(subject.author).to eq(author)
expect(subject.created_at).to eq(event.created_at)
+ expect(subject.updated_at).to eq(event.created_at)
expect(subject.note).to eq(state)
end
end
@@ -33,7 +34,8 @@ RSpec.describe StateNote do
it 'contains the expected values' do
expect(subject.author).to eq(author)
- expect(subject.created_at).to eq(subject.created_at)
+ expect(subject.created_at).to eq(event.created_at)
+ expect(subject.updated_at).to eq(event.created_at)
expect(subject.note).to eq("closed via commit #{commit.id}")
end
end
@@ -45,6 +47,7 @@ RSpec.describe StateNote do
it 'contains the expected values' do
expect(subject.author).to eq(author)
expect(subject.created_at).to eq(event.created_at)
+ expect(subject.updated_at).to eq(event.created_at)
expect(subject.note).to eq("closed via merge request !#{merge_request.iid}")
end
end
@@ -55,6 +58,7 @@ RSpec.describe StateNote do
it 'contains the expected values' do
expect(subject.author).to eq(author)
expect(subject.created_at).to eq(event.created_at)
+ expect(subject.updated_at).to eq(event.created_at)
expect(subject.note).to eq('resolved the corresponding error and closed the issue')
end
end
@@ -65,6 +69,7 @@ RSpec.describe StateNote do
it 'contains the expected values' do
expect(subject.author).to eq(author)
expect(subject.created_at).to eq(event.created_at)
+ expect(subject.updated_at).to eq(event.created_at)
expect(subject.note).to eq('automatically closed this incident because the alert resolved')
end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 23ba0be2fbc..221f09dd87f 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -56,6 +56,15 @@ RSpec.describe Todo do
expect(subject.body).to eq 'quick fix'
end
+
+ it 'returns full path of target when action is member_access_requested' do
+ group = create(:group)
+
+ subject.target = group
+ subject.action = Todo::MEMBER_ACCESS_REQUESTED
+
+ expect(subject.body).to eq group.full_path
+ end
end
describe '#done' do
@@ -182,6 +191,17 @@ RSpec.describe Todo do
expect(subject.target_reference).to eq issue.to_reference(full: false)
end
+
+ context 'when target is member access requested' do
+ it 'returns group full path' do
+ group = create(:group)
+
+ subject.target = group
+ subject.action = Todo::MEMBER_ACCESS_REQUESTED
+
+ expect(subject.target_reference).to eq group.full_path
+ end
+ end
end
describe '#self_added?' do
diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb
index 04964d36dcd..ed55aca49b7 100644
--- a/spec/models/user_detail_spec.rb
+++ b/spec/models/user_detail_spec.rb
@@ -48,6 +48,23 @@ RSpec.describe UserDetail do
describe '#website_url' do
it { is_expected.to validate_length_of(:website_url).is_at_most(500) }
+
+ it 'only validates the website_url if it is changed' do
+ user_detail = create(:user_detail)
+ # `update_attribute` required to bypass current validations
+ # Validations on `User#website_url` were added after
+ # there was already data in the database and `UserDetail#website_url` is
+ # derived from `User#website_url` so this reproduces the state of some of
+ # our production data
+ user_detail.update_attribute(:website_url, 'NotAUrl')
+
+ expect(user_detail).to be_valid
+
+ user_detail.website_url = 'AlsoNotAUrl'
+
+ expect(user_detail).not_to be_valid
+ expect(user_detail.errors.full_messages).to match_array(["Website url is not a valid URL"])
+ end
end
end
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index d76334d7c9e..a6f64c90657 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
RSpec.describe UserPreference do
- let(:user_preference) { create(:user_preference) }
+ let_it_be(:user) { create(:user) }
+
+ let(:user_preference) { create(:user_preference, user: user) }
describe 'validations' do
describe 'diffs_deletion_color and diffs_addition_color' do
@@ -132,10 +134,24 @@ RSpec.describe UserPreference do
describe '#tab_width' do
it 'is set to 8 by default' do
# Intentionally not using factory here to test the constructor.
- pref = UserPreference.new
+ pref = described_class.new
+
+ expect(pref.tab_width).to eq(8)
+ end
+
+ it 'returns default value when assigning nil' do
+ pref = described_class.new(tab_width: nil)
+
expect(pref.tab_width).to eq(8)
end
+ it 'returns default value when the value is NULL' do
+ pref = create(:user_preference, user: user)
+ pref.update_column(:tab_width, nil)
+
+ expect(pref.reload.tab_width).to eq(8)
+ end
+
it do
is_expected.to validate_numericality_of(:tab_width)
.only_integer
@@ -143,4 +159,141 @@ RSpec.describe UserPreference do
.is_less_than_or_equal_to(12)
end
end
+
+ describe '#tab_width=' do
+ it 'sets to default value when nil' do
+ pref = described_class.new(tab_width: nil)
+
+ expect(pref.read_attribute(:tab_width)).to eq(8)
+ end
+
+ it 'sets user values' do
+ pref = described_class.new(tab_width: 12)
+
+ expect(pref.read_attribute(:tab_width)).to eq(12)
+ end
+ end
+
+ describe '#time_display_relative' do
+ it 'is set to true by default' do
+ pref = described_class.new
+
+ expect(pref.time_display_relative).to eq(true)
+ end
+
+ it 'returns default value when assigning nil' do
+ pref = described_class.new(time_display_relative: nil)
+
+ expect(pref.time_display_relative).to eq(true)
+ end
+
+ it 'returns default value when the value is NULL' do
+ pref = create(:user_preference, user: user)
+ pref.update_column(:time_display_relative, nil)
+
+ expect(pref.reload.time_display_relative).to eq(true)
+ end
+
+ it 'returns assigned value' do
+ pref = described_class.new(time_display_relative: false)
+
+ expect(pref.time_display_relative).to eq(false)
+ end
+ end
+
+ describe '#time_display_relative=' do
+ it 'sets to default value when nil' do
+ pref = described_class.new(time_display_relative: nil)
+
+ expect(pref.read_attribute(:time_display_relative)).to eq(true)
+ end
+
+ it 'sets user values' do
+ pref = described_class.new(time_display_relative: false)
+
+ expect(pref.read_attribute(:time_display_relative)).to eq(false)
+ end
+ end
+
+ describe '#time_format_in_24h' do
+ it 'is set to false by default' do
+ pref = described_class.new
+
+ expect(pref.time_format_in_24h).to eq(false)
+ end
+
+ it 'returns default value when assigning nil' do
+ pref = described_class.new(time_format_in_24h: nil)
+
+ expect(pref.time_format_in_24h).to eq(false)
+ end
+
+ it 'returns default value when the value is NULL' do
+ pref = create(:user_preference, user: user)
+ pref.update_column(:time_format_in_24h, nil)
+
+ expect(pref.reload.time_format_in_24h).to eq(false)
+ end
+
+ it 'returns assigned value' do
+ pref = described_class.new(time_format_in_24h: true)
+
+ expect(pref.time_format_in_24h).to eq(true)
+ end
+ end
+
+ describe '#time_format_in_24h=' do
+ it 'sets to default value when nil' do
+ pref = described_class.new(time_format_in_24h: nil)
+
+ expect(pref.read_attribute(:time_format_in_24h)).to eq(false)
+ end
+
+ it 'sets user values' do
+ pref = described_class.new(time_format_in_24h: true)
+
+ expect(pref.read_attribute(:time_format_in_24h)).to eq(true)
+ end
+ end
+
+ describe '#render_whitespace_in_code' do
+ it 'is set to false by default' do
+ pref = described_class.new
+
+ expect(pref.render_whitespace_in_code).to eq(false)
+ end
+
+ it 'returns default value when assigning nil' do
+ pref = described_class.new(render_whitespace_in_code: nil)
+
+ expect(pref.render_whitespace_in_code).to eq(false)
+ end
+
+ it 'returns default value when the value is NULL' do
+ pref = create(:user_preference, user: user)
+ pref.update_column(:render_whitespace_in_code, nil)
+
+ expect(pref.reload.render_whitespace_in_code).to eq(false)
+ end
+
+ it 'returns assigned value' do
+ pref = described_class.new(render_whitespace_in_code: true)
+
+ expect(pref.render_whitespace_in_code).to eq(true)
+ end
+ end
+
+ describe '#render_whitespace_in_code=' do
+ it 'sets to default value when nil' do
+ pref = described_class.new(render_whitespace_in_code: nil)
+
+ expect(pref.read_attribute(:render_whitespace_in_code)).to eq(false)
+ end
+
+ it 'sets user values' do
+ pref = described_class.new(render_whitespace_in_code: true)
+
+ expect(pref.read_attribute(:render_whitespace_in_code)).to eq(true)
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 7207ee0b172..4a66af4ddf1 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -81,6 +81,9 @@ RSpec.describe User do
it { is_expected.to delegate_method(:use_legacy_web_ide).to(:user_preference) }
it { is_expected.to delegate_method(:use_legacy_web_ide=).to(:user_preference).with_arguments(:args) }
+ it { is_expected.to delegate_method(:use_new_navigation).to(:user_preference) }
+ it { is_expected.to delegate_method(:use_new_navigation=).to(:user_preference).with_arguments(:args) }
+
it { is_expected.to delegate_method(:job_title).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:job_title=).to(:user_detail).with_arguments(:args).allow_nil }
@@ -146,6 +149,21 @@ RSpec.describe User do
it { is_expected.to have_many(:project_callouts).class_name('Users::ProjectCallout') }
it { is_expected.to have_many(:created_projects).dependent(:nullify).class_name('Project') }
+ describe 'default values' do
+ let(:user) { described_class.new }
+
+ it { expect(user.admin).to be_falsey }
+ it { expect(user.external).to eq(Gitlab::CurrentSettings.user_default_external) }
+ it { expect(user.can_create_group).to eq(Gitlab::CurrentSettings.can_create_group) }
+ it { expect(user.can_create_team).to be_falsey }
+ it { expect(user.hide_no_ssh_key).to be_falsey }
+ it { expect(user.hide_no_password).to be_falsey }
+ it { expect(user.project_view).to eq('files') }
+ it { expect(user.notified_of_own_activity).to be_falsey }
+ it { expect(user.preferred_language).to eq(I18n.default_locale.to_s) }
+ it { expect(user.theme_id).to eq(described_class.gitlab_config.default_theme) }
+ end
+
describe '#user_detail' do
it 'does not persist `user_detail` by default' do
expect(create(:user).user_detail).not_to be_persisted
@@ -345,52 +363,33 @@ RSpec.describe User do
context 'check_password_weakness' do
let(:weak_password) { "qwertyuiop" }
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(block_weak_passwords: false)
- end
-
- it 'does not add an error when password is weak' do
- expect(Security::WeakPasswords).not_to receive(:weak_for_user?)
-
- user.password = weak_password
- expect(user).to be_valid
- end
+ it 'checks for password weakness when password changes' do
+ expect(Security::WeakPasswords).to receive(:weak_for_user?)
+ .with(weak_password, user).and_call_original
+ user.password = weak_password
+ expect(user).not_to be_valid
end
- context 'when feature flag is enabled' do
- before do
- stub_feature_flags(block_weak_passwords: true)
- end
-
- it 'checks for password weakness when password changes' do
- expect(Security::WeakPasswords).to receive(:weak_for_user?)
- .with(weak_password, user).and_call_original
- user.password = weak_password
- expect(user).not_to be_valid
- end
-
- it 'adds an error when password is weak' do
- user.password = weak_password
- expect(user).not_to be_valid
- expect(user.errors).to be_of_kind(:password, 'must not contain commonly used combinations of words and letters')
- end
+ it 'adds an error when password is weak' do
+ user.password = weak_password
+ expect(user).not_to be_valid
+ expect(user.errors).to be_of_kind(:password, 'must not contain commonly used combinations of words and letters')
+ end
- it 'is valid when password is not weak' do
- user.password = ::User.random_password
- expect(user).to be_valid
- end
+ it 'is valid when password is not weak' do
+ user.password = ::User.random_password
+ expect(user).to be_valid
+ end
- it 'is valid when weak password was already set' do
- user = build(:user, password: weak_password)
- user.save!(validate: false)
+ it 'is valid when weak password was already set' do
+ user = build(:user, password: weak_password)
+ user.save!(validate: false)
- expect(Security::WeakPasswords).not_to receive(:weak_for_user?)
+ expect(Security::WeakPasswords).not_to receive(:weak_for_user?)
- # Change an unrelated value
- user.name = "Example McExampleFace"
- expect(user).to be_valid
- end
+ # Change an unrelated value
+ user.name = "Example McExampleFace"
+ expect(user).to be_valid
end
end
end
@@ -417,7 +416,7 @@ RSpec.describe User do
end
it 'falls back to english when I18n.default_locale is not an available language' do
- I18n.default_locale = :kl
+ allow(I18n).to receive(:default_locale) { :kl }
default_preferred_language = user.send(:default_preferred_language)
expect(user.preferred_language).to eq default_preferred_language
@@ -1590,10 +1589,6 @@ RSpec.describe User do
let(:expired_confirmation_sent_at) { Date.today - described_class.confirm_within - 7.days }
let(:extant_confirmation_sent_at) { Date.today }
- before do
- allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
- end
-
let(:user) do
create(:user, :unconfirmed, unconfirmed_email: 'test@gitlab.com').tap do |user|
user.update!(confirmation_sent_at: confirmation_sent_at)
@@ -3090,6 +3085,14 @@ RSpec.describe User do
expect(described_class.find_by_ssh_key_id(-1)).to be_nil
end
end
+
+ it 'does not return a signing-only key', :aggregate_failures do
+ signing_key = create(:key, usage_type: :signing, user: user)
+ auth_and_signing_key = create(:key, usage_type: :auth_and_signing, user: user)
+
+ expect(described_class.find_by_ssh_key_id(signing_key.id)).to be_nil
+ expect(described_class.find_by_ssh_key_id(auth_and_signing_key.id)).to eq(user)
+ end
end
shared_examples "find user by login" do
@@ -3209,6 +3212,7 @@ RSpec.describe User do
expect(described_class.find_by_full_path('unknown')).to eq(nil)
end
end
+
context 'with the follow_redirects option set to true' do
it 'returns nil' do
expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to eq(nil)
@@ -4431,7 +4435,7 @@ RSpec.describe User do
shared_context '#ci_owned_runners' do
let(:user) { create(:user) }
- shared_examples :nested_groups_owner do
+ shared_examples 'nested groups owner' do
context 'when the user is the owner of a multi-level group' do
before do
set_permissions_for_users
@@ -4448,7 +4452,7 @@ RSpec.describe User do
end
end
- shared_examples :group_owner do
+ shared_examples 'group owner' do
context 'when the user is the owner of a one level group' do
before do
group.add_owner(user)
@@ -4464,7 +4468,7 @@ RSpec.describe User do
end
end
- shared_examples :project_owner do
+ shared_examples 'project owner' do
context 'when the user is the owner of a project' do
it 'loads the runner belonging to the project' do
expect(user.ci_owned_runners).to contain_exactly(runner)
@@ -4476,7 +4480,7 @@ RSpec.describe User do
end
end
- shared_examples :project_member do
+ shared_examples 'project member' do
context 'when the user is a maintainer' do
before do
add_user(:maintainer)
@@ -4534,7 +4538,7 @@ RSpec.describe User do
end
end
- shared_examples :group_member do
+ shared_examples 'group member' do
context 'when the user is a maintainer' do
before do
add_user(:maintainer)
@@ -4607,7 +4611,7 @@ RSpec.describe User do
let!(:project) { create(:project, namespace: namespace) }
let!(:runner) { create(:ci_runner, :project, projects: [project]) }
- it_behaves_like :project_owner
+ it_behaves_like 'project owner'
end
context 'with group runner in a non owned group' do
@@ -4618,14 +4622,14 @@ RSpec.describe User do
group.add_member(user, access)
end
- it_behaves_like :group_member
+ it_behaves_like 'group member'
end
context 'with group runner in an owned group' do
let!(:group) { create(:group) }
let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
- it_behaves_like :group_owner
+ it_behaves_like 'group owner'
end
context 'with group runner in an owned group and group runner in a different owner subgroup' do
@@ -4640,7 +4644,7 @@ RSpec.describe User do
subgroup.add_owner(another_user)
end
- it_behaves_like :nested_groups_owner
+ it_behaves_like 'nested groups owner'
end
context 'with personal project runner in an an owned group and a group runner in that same group' do
@@ -4653,7 +4657,7 @@ RSpec.describe User do
group.add_owner(user)
end
- it_behaves_like :nested_groups_owner
+ it_behaves_like 'nested groups owner'
end
context 'with personal project runner in an owned group and a group runner in a subgroup' do
@@ -4667,7 +4671,7 @@ RSpec.describe User do
group.add_owner(user)
end
- it_behaves_like :nested_groups_owner
+ it_behaves_like 'nested groups owner'
end
context 'with personal project runner in an owned group in an owned namespace and a group runner in that group' do
@@ -4681,7 +4685,7 @@ RSpec.describe User do
group.add_owner(user)
end
- it_behaves_like :nested_groups_owner
+ it_behaves_like 'nested groups owner'
end
context 'with personal project runner in an owned namespace, an owned group, a subgroup and a group runner in that subgroup' do
@@ -4696,7 +4700,7 @@ RSpec.describe User do
group.add_owner(user)
end
- it_behaves_like :nested_groups_owner
+ it_behaves_like 'nested groups owner'
end
context 'with a project runner that belong to projects that belong to a not owned group' do
@@ -4708,7 +4712,7 @@ RSpec.describe User do
project.add_member(user, access)
end
- it_behaves_like :project_member
+ it_behaves_like 'project member'
end
context 'with project runners that belong to projects that do not belong to any group' do
@@ -4731,7 +4735,7 @@ RSpec.describe User do
group.add_member(another_user, :owner)
end
- it_behaves_like :group_member
+ it_behaves_like 'group member'
end
end
@@ -5221,6 +5225,10 @@ RSpec.describe User do
describe '#invalidate_issue_cache_counts' do
let(:user) { build_stubbed(:user) }
+ before do
+ stub_feature_flags(limit_assigned_issues_count: false)
+ end
+
it 'invalidates cache for issue counter' do
cache_mock = double
@@ -5230,6 +5238,23 @@ RSpec.describe User do
user.invalidate_issue_cache_counts
end
+
+ context 'when limit_assigned_issues_count is enabled' do
+ before do
+ stub_feature_flags(limit_assigned_issues_count: true)
+ end
+
+ it 'invalidates cache for issue counter' do
+ cache_mock = double
+
+ expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count'])
+ expect(cache_mock).to receive(:delete).with(['users', user.id, 'max_assigned_open_issues_count'])
+
+ allow(Rails).to receive(:cache).and_return(cache_mock)
+
+ user.invalidate_issue_cache_counts
+ end
+ end
end
describe '#invalidate_merge_request_cache_counts' do
@@ -6155,33 +6180,44 @@ RSpec.describe User do
describe '#notification_email_for' do
let(:user) { create(:user) }
- let(:group) { create(:group) }
- subject { user.notification_email_for(group) }
+ subject { user.notification_email_for(namespace) }
- context 'when group is nil' do
- let(:group) { nil }
+ context 'when namespace is nil' do
+ let(:namespace) { nil }
it 'returns global notification email' do
is_expected.to eq(user.notification_email_or_default)
end
end
- context 'when group has no notification email set' do
- it 'returns global notification email' do
- create(:notification_setting, user: user, source: group, notification_email: '')
+ context 'for group namespace' do
+ let(:namespace) { create(:group) }
- is_expected.to eq(user.notification_email_or_default)
+ context 'when group has no notification email set' do
+ it 'returns global notification email' do
+ create(:notification_setting, user: user, source: namespace, notification_email: '')
+
+ is_expected.to eq(user.notification_email_or_default)
+ end
+ end
+
+ context 'when group has notification email set' do
+ it 'returns group notification email' do
+ group_notification_email = 'user+group@example.com'
+ create(:email, :confirmed, user: user, email: group_notification_email)
+ create(:notification_setting, user: user, source: namespace, notification_email: group_notification_email)
+
+ is_expected.to eq(group_notification_email)
+ end
end
end
- context 'when group has notification email set' do
- it 'returns group notification email' do
- group_notification_email = 'user+group@example.com'
- create(:email, :confirmed, user: user, email: group_notification_email)
- create(:notification_setting, user: user, source: group, notification_email: group_notification_email)
+ context 'for user namespace' do
+ let(:namespace) { create(:user_namespace) }
- is_expected.to eq(group_notification_email)
+ it 'returns global notification email' do
+ is_expected.to eq(user.notification_email_or_default)
end
end
end
@@ -6799,7 +6835,8 @@ RSpec.describe User do
{ user_type: :alert_bot },
{ user_type: :support_bot },
{ user_type: :security_bot },
- { user_type: :automation_bot }
+ { user_type: :automation_bot },
+ { user_type: :admin_bot }
]
end
@@ -6881,11 +6918,12 @@ RSpec.describe User do
using RSpec::Parameterized::TableSyntax
where(:user_type, :expected_result) do
- 'human' | true
- 'alert_bot' | false
- 'support_bot' | false
- 'security_bot' | false
- 'automation_bot' | false
+ 'human' | true
+ 'alert_bot' | false
+ 'support_bot' | false
+ 'security_bot' | false
+ 'automation_bot' | false
+ 'admin_bot' | false
end
with_them do
@@ -7034,17 +7072,26 @@ RSpec.describe User do
it_behaves_like 'bot users', :security_bot
it_behaves_like 'bot users', :ghost
it_behaves_like 'bot users', :automation_bot
+ it_behaves_like 'bot users', :admin_bot
it_behaves_like 'bot user avatars', :alert_bot, 'alert-bot.png'
it_behaves_like 'bot user avatars', :support_bot, 'support-bot.png'
it_behaves_like 'bot user avatars', :security_bot, 'security-bot.png'
it_behaves_like 'bot user avatars', :automation_bot, 'support-bot.png'
+ it_behaves_like 'bot user avatars', :admin_bot, 'admin-bot.png'
context 'when bot is the support_bot' do
subject { described_class.support_bot }
it { is_expected.to be_confirmed }
end
+
+ context 'when bot is the admin bot' do
+ subject { described_class.admin_bot }
+
+ it { is_expected.to be_admin }
+ it { is_expected.to be_confirmed }
+ end
end
describe '#confirmation_required_on_sign_in?' do
@@ -7307,4 +7354,51 @@ RSpec.describe User do
expect(user.account_age_in_days).to be(1)
end
end
+
+ describe 'state machine and default attributes' do
+ let(:model) do
+ Class.new(ApplicationRecord) do
+ self.table_name = User.table_name
+
+ attribute :external, default: -> { 1 / 0 }
+
+ state_machine :state, initial: :active do
+ end
+ end
+ end
+
+ it 'raises errors by default' do
+ expect { model }.to raise_error(ZeroDivisionError)
+ end
+
+ context 'with state machine default attributes override' do
+ let(:model) do
+ Class.new(ApplicationRecord) do
+ self.table_name = User.table_name
+
+ attribute :external, default: -> { 1 / 0 }
+
+ state_machine :state, initial: :active do
+ def owner_class_attribute_default; end
+ end
+ end
+ end
+
+ it 'does not raise errors' do
+ expect { model }.not_to raise_error
+ end
+
+ it 'raises errors when default attributes are used' do
+ expect { model.new.attributes }.to raise_error(ZeroDivisionError)
+ end
+
+ it 'does not evaluate default attributes when values are provided' do
+ expect { model.new(external: false).attributes }.not_to raise_error
+ end
+
+ it 'sets the state machine default value' do
+ expect(model.new(external: true).state).to eq('active')
+ end
+ end
+ end
end
diff --git a/spec/models/users/phone_number_validation_spec.rb b/spec/models/users/phone_number_validation_spec.rb
index 2f0fd1d3ac9..7ab461a4346 100644
--- a/spec/models/users/phone_number_validation_spec.rb
+++ b/spec/models/users/phone_number_validation_spec.rb
@@ -78,4 +78,42 @@ RSpec.describe Users::PhoneNumberValidation do
it { is_expected.to eq(false) }
end
end
+
+ describe '#for_user' do
+ let_it_be(:user_1) { create(:user) }
+ let_it_be(:user_2) { 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) }
+
+ 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)
+
+ expect(records.count).to be(1)
+ expect(records.first).to eq(phone_number_record_1)
+ 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
+ it 'returns false' do
+ expect(phone_number_record.validated?).to be(false)
+ end
+ end
+
+ context 'when phone number record is validated' do
+ before do
+ phone_number_record.update!(validated_at: Time.now.utc)
+ end
+
+ it 'returns true' do
+ expect(phone_number_record.validated?).to be(true)
+ end
+ end
+ end
end
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index 341f9a9c60f..1c34936c5c2 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItem do
+RSpec.describe WorkItem, feature_category: :portfolio_management do
let_it_be(:reusable_project) { create(:project) }
describe 'associations' do
@@ -176,4 +176,59 @@ RSpec.describe WorkItem do
end
end
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) }
+ let_it_be(:type3) { create(:work_item_type, namespace: reusable_project.namespace) }
+ let_it_be(:type4) { create(:work_item_type, namespace: reusable_project.namespace) }
+ let_it_be(:hierarchy_restriction1) { create(:hierarchy_restriction, parent_type: type1, child_type: type2) }
+ let_it_be(:hierarchy_restriction2) { create(:hierarchy_restriction, parent_type: type2, child_type: type2) }
+ let_it_be(:hierarchy_restriction3) { create(:hierarchy_restriction, parent_type: type2, child_type: type3) }
+ let_it_be(:hierarchy_restriction4) { create(:hierarchy_restriction, parent_type: type3, child_type: type3) }
+ let_it_be(:hierarchy_restriction5) { create(:hierarchy_restriction, parent_type: type3, child_type: type4) }
+ let_it_be(:item1) { create(:work_item, work_item_type: type1, project: reusable_project) }
+ let_it_be(:item2_1) { create(:work_item, work_item_type: type2, project: reusable_project) }
+ let_it_be(:item2_2) { create(:work_item, work_item_type: type2, project: reusable_project) }
+ let_it_be(:item3_1) { create(:work_item, work_item_type: type3, project: reusable_project) }
+ let_it_be(:item3_2) { create(:work_item, work_item_type: type3, project: reusable_project) }
+ let_it_be(:item4) { create(:work_item, work_item_type: type4, project: reusable_project) }
+ let_it_be(:ignored_ancestor) { create(:work_item, work_item_type: type1, project: reusable_project) }
+ let_it_be(:ignored_descendant) { create(:work_item, work_item_type: type4, project: reusable_project) }
+ let_it_be(:link1) { create(:parent_link, work_item_parent: item1, work_item: item2_1) }
+ let_it_be(:link2) { create(:parent_link, work_item_parent: item2_1, work_item: item2_2) }
+ let_it_be(:link3) { create(:parent_link, work_item_parent: item2_2, work_item: item3_1) }
+ let_it_be(:link4) { create(:parent_link, work_item_parent: item3_1, work_item: item3_2) }
+ let_it_be(:link5) { create(:parent_link, work_item_parent: item3_2, work_item: item4) }
+
+ describe '#ancestors' do
+ it 'returns all ancestors in ascending order' do
+ expect(item3_1.ancestors).to eq([item2_2, item2_1, item1])
+ end
+
+ it 'returns an empty array if there are no ancestors' do
+ expect(item1.ancestors).to be_empty
+ end
+ end
+
+ describe '#same_type_base_and_ancestors' do
+ it 'returns self and all ancestors of the same type in ascending order' do
+ expect(item3_2.same_type_base_and_ancestors).to eq([item3_2, item3_1])
+ end
+
+ it 'returns self if there are no ancestors of the same type' do
+ expect(item3_1.same_type_base_and_ancestors).to match_array([item3_1])
+ end
+ end
+
+ describe '#same_type_descendants_depth' do
+ it 'returns max descendants depth including self' do
+ expect(item3_1.same_type_descendants_depth).to eq(2)
+ end
+
+ it 'returns 1 if there are no descendants' do
+ expect(item1.same_type_descendants_depth).to eq(1)
+ end
+ end
+ end
end
diff --git a/spec/models/work_items/hierarchy_restriction_spec.rb b/spec/models/work_items/hierarchy_restriction_spec.rb
new file mode 100644
index 00000000000..2c4d5d32fb8
--- /dev/null
+++ b/spec/models/work_items/hierarchy_restriction_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::HierarchyRestriction do
+ describe 'associations' do
+ it { is_expected.to belong_to(:parent_type) }
+ it { is_expected.to belong_to(:child_type) }
+ end
+
+ describe 'validations' do
+ subject { build(:hierarchy_restriction) }
+
+ it { is_expected.to validate_presence_of(:parent_type) }
+ it { is_expected.to validate_presence_of(:child_type) }
+ it { is_expected.to validate_uniqueness_of(:child_type).scoped_to(:parent_type_id) }
+ end
+end
diff --git a/spec/models/work_items/parent_link_spec.rb b/spec/models/work_items/parent_link_spec.rb
index 070b2eef86a..82e79e8fbdf 100644
--- a/spec/models/work_items/parent_link_spec.rb
+++ b/spec/models/work_items/parent_link_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe WorkItems::ParentLink do
+RSpec.describe WorkItems::ParentLink, feature_category: :portfolio_management do
+ let_it_be(:project) { create(:project) }
+
describe 'associations' do
it { is_expected.to belong_to(:work_item) }
it { is_expected.to belong_to(:work_item_parent).class_name('WorkItem') }
@@ -16,7 +18,6 @@ RSpec.describe WorkItems::ParentLink do
it { is_expected.to validate_uniqueness_of(:work_item) }
describe 'hierarchy' do
- let_it_be(:project) { create(:project) }
let_it_be(:issue) { build(:work_item, project: project) }
let_it_be(:incident) { build(:work_item, :incident, project: project) }
let_it_be(:task1) { build(:work_item, :task, project: project) }
@@ -30,18 +31,82 @@ RSpec.describe WorkItems::ParentLink do
expect(build(:parent_link, work_item: task1, work_item_parent: incident)).to be_valid
end
- it 'is not valid if child is not task' do
- link = build(:parent_link, work_item: issue)
+ context 'when assigning to various parent types' do
+ using RSpec::Parameterized::TableSyntax
- expect(link).not_to be_valid
- expect(link.errors[:work_item]).to include('only Task can be assigned as a child in hierarchy.')
+ where(:parent_type_sym, :child_type_sym, :is_valid) do
+ :issue | :task | true
+ :incident | :task | true
+ :task | :issue | false
+ :issue | :issue | false
+ :objective | :objective | true
+ :objective | :key_result | true
+ :key_result | :objective | false
+ :key_result | :key_result | false
+ :objective | :issue | false
+ :task | :objective | false
+ end
+
+ with_them do
+ it 'validates if child can be added to the parent' do
+ parent_type = WorkItems::Type.default_by_type(parent_type_sym)
+ child_type = WorkItems::Type.default_by_type(child_type_sym)
+ parent = build(:work_item, issue_type: parent_type_sym, work_item_type: parent_type, project: project)
+ child = build(:work_item, issue_type: child_type_sym, work_item_type: child_type, project: project)
+ link = build(:parent_link, work_item: child, work_item_parent: parent)
+
+ expect(link.valid?).to eq(is_valid)
+ end
+ end
end
- it 'is not valid if parent is task' do
- link = build(:parent_link, work_item_parent: task1)
+ context 'with nested ancestors' do
+ let_it_be(:type1) { create(:work_item_type, namespace: project.namespace) }
+ let_it_be(:type2) { create(:work_item_type, namespace: project.namespace) }
+ let_it_be(:item1) { create(:work_item, work_item_type: type1, project: project) }
+ let_it_be(:item2) { create(:work_item, work_item_type: type2, project: project) }
+ let_it_be(:item3) { create(:work_item, work_item_type: type2, project: project) }
+ let_it_be(:item4) { create(:work_item, work_item_type: type2, project: project) }
+ let_it_be(:hierarchy_restriction1) { create(:hierarchy_restriction, parent_type: type1, child_type: type2) }
+ let_it_be(:hierarchy_restriction2) { create(:hierarchy_restriction, parent_type: type2, child_type: type1) }
+
+ let_it_be(:hierarchy_restriction3) do
+ create(:hierarchy_restriction, parent_type: type2, child_type: type2, maximum_depth: 2)
+ end
- expect(link).not_to be_valid
- expect(link.errors[:work_item_parent]).to include('only Issue and Incident can be parent of Task.')
+ let_it_be(:link1) { create(:parent_link, work_item_parent: item1, work_item: item2) }
+ let_it_be(:link2) { create(:parent_link, work_item_parent: item3, work_item: item4) }
+
+ describe '#validate_depth' do
+ it 'is valid if depth is in limit' do
+ link = build(:parent_link, work_item_parent: item1, work_item: item3)
+
+ expect(link).to be_valid
+ end
+
+ it 'is not valid when maximum depth is reached' do
+ link = build(:parent_link, work_item_parent: item2, work_item: item3)
+
+ expect(link).not_to be_valid
+ expect(link.errors[:work_item]).to include('reached maximum depth')
+ end
+ end
+
+ describe '#validate_cyclic_reference' do
+ it 'is not valid if parent and child are same' do
+ link1.work_item_parent = item2
+
+ expect(link1).not_to be_valid
+ expect(link1.errors[:work_item]).to include('is not allowed to point to itself')
+ end
+
+ it 'is not valid if child is already in ancestors' do
+ link = build(:parent_link, work_item_parent: item4, work_item: item3)
+
+ expect(link).not_to be_valid
+ expect(link.errors[:work_item]).to include('is already present in ancestors')
+ end
+ end
end
it 'is not valid if parent is in other project' do
@@ -96,8 +161,26 @@ RSpec.describe WorkItems::ParentLink do
end
end
- context 'with confidential work items' do
+ describe 'scopes' do
let_it_be(:project) { create(:project) }
+ let_it_be(:issue1) { build(:work_item, project: project) }
+ let_it_be(:issue2) { build(:work_item, project: project) }
+ let_it_be(:issue3) { build(:work_item, project: project) }
+ let_it_be(:task1) { build(:work_item, :task, project: project) }
+ let_it_be(:task2) { build(:work_item, :task, project: project) }
+ let_it_be(:link1) { create(:parent_link, work_item_parent: issue1, work_item: task1) }
+ let_it_be(:link2) { create(:parent_link, work_item_parent: issue2, work_item: task2) }
+
+ describe 'for_parents' do
+ it 'includes the correct records' do
+ result = described_class.for_parents([issue1.id, issue2.id, issue3.id])
+
+ expect(result).to include(link1, link2)
+ end
+ end
+ end
+
+ context 'with confidential work items' do
let_it_be(:confidential_child) { create(:work_item, :task, confidential: true, project: project) }
let_it_be(:putlic_child) { create(:work_item, :task, project: project) }
let_it_be(:confidential_parent) { create(:work_item, confidential: true, project: project) }
diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb
index 6685720778a..1ada783385e 100644
--- a/spec/models/work_items/type_spec.rb
+++ b/spec/models/work_items/type_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe WorkItems::Type do
end
it 'does not delete type when there are related issues' do
- type = create(:work_item_type, work_items: [work_item])
+ type = work_item.work_item_type
expect { type.destroy! }.to raise_error(ActiveRecord::InvalidForeignKey)
expect(Issue.count).to eq(1)
@@ -70,11 +70,38 @@ RSpec.describe WorkItems::Type do
::WorkItems::Widgets::Labels,
::WorkItems::Widgets::Assignees,
::WorkItems::Widgets::StartAndDueDate,
- ::WorkItems::Widgets::Milestone
+ ::WorkItems::Widgets::Milestone,
+ ::WorkItems::Widgets::Notes
)
end
end
+ describe '.default_by_type' do
+ let(:default_issue_type) { described_class.find_by(namespace_id: nil, base_type: :issue) }
+
+ subject { described_class.default_by_type(:issue) }
+
+ it 'returns default work item type by base type without calling importer' do
+ expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).not_to receive(:upsert_types)
+ expect(Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter).not_to receive(:upsert_restrictions)
+
+ expect(subject).to eq(default_issue_type)
+ end
+
+ context 'when default types are missing' do
+ before do
+ described_class.delete_all
+ end
+
+ it 'creates types and restrictions and returns default work item type by base type' do
+ expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).to receive(:upsert_types)
+ expect(Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter).to receive(:upsert_restrictions)
+
+ expect(subject).to eq(default_issue_type)
+ end
+ end
+ end
+
describe '#default?' do
subject { build(:work_item_type, namespace: namespace).default? }
diff --git a/spec/models/work_items/widgets/notes_spec.rb b/spec/models/work_items/widgets/notes_spec.rb
new file mode 100644
index 00000000000..cc98f1ebe54
--- /dev/null
+++ b/spec/models/work_items/widgets/notes_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::Notes, feature_category: :team_planning do
+ let_it_be(:work_item) { create(:work_item) }
+ let_it_be(:note) { create(:note, noteable: work_item, project: work_item.project) }
+
+ describe '.type' do
+ it { expect(described_class.type).to eq(:notes) }
+ end
+
+ describe '#type' do
+ it { expect(described_class.new(work_item).type).to eq(:notes) }
+ end
+
+ describe '#notes' do
+ it { expect(described_class.new(work_item).notes).to eq(work_item.notes) }
+ end
+end
diff --git a/spec/models/zoom_meeting_spec.rb b/spec/models/zoom_meeting_spec.rb
index 2b45533035d..d3d75a19fed 100644
--- a/spec/models/zoom_meeting_spec.rb
+++ b/spec/models/zoom_meeting_spec.rb
@@ -29,6 +29,7 @@ RSpec.describe ZoomMeeting do
expect(meetings_added).not_to include(removed_meeting.id)
end
end
+
describe '.removed_from_issue' do
it 'gets only removed meetings' do
meetings_removed = described_class.removed_from_issue.pluck(:id)
diff --git a/spec/policies/ci/runner_policy_spec.rb b/spec/policies/ci/runner_policy_spec.rb
index 773d3d9a01d..6039d60ec2f 100644
--- a/spec/policies/ci/runner_policy_spec.rb
+++ b/spec/policies/ci/runner_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::RunnerPolicy do
+RSpec.describe Ci::RunnerPolicy, feature_category: :runner do
describe 'ability :read_runner' do
let_it_be(:guest) { create(:user) }
let_it_be(:developer) { create(:user) }
diff --git a/spec/policies/concerns/archived_abilities_spec.rb b/spec/policies/concerns/archived_abilities_spec.rb
new file mode 100644
index 00000000000..8e3fd8a209f
--- /dev/null
+++ b/spec/policies/concerns/archived_abilities_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ArchivedAbilities, feature_category: :projects do
+ let(:test_class) do
+ Class.new do
+ include ArchivedAbilities
+ end
+ end
+
+ before do
+ stub_const('TestClass', test_class)
+ end
+
+ describe '.archived_abilities' do
+ it 'returns an array of abilites to be prevented when archived' do
+ expect(TestClass.archived_abilities).to include(*described_class::ARCHIVED_ABILITIES)
+ end
+ end
+
+ describe '.archived_features' do
+ it 'returns an array of features to be prevented when archived' do
+ expect(TestClass.archived_features).to include(*described_class::ARCHIVED_FEATURES)
+ end
+ end
+end
diff --git a/spec/policies/concerns/readonly_abilities_spec.rb b/spec/policies/concerns/readonly_abilities_spec.rb
deleted file mode 100644
index 864924a091d..00000000000
--- a/spec/policies/concerns/readonly_abilities_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ReadonlyAbilities do
- let(:test_class) do
- Class.new do
- include ReadonlyAbilities
- end
- end
-
- before do
- stub_const('TestClass', test_class)
- end
-
- describe '.readonly_abilities' do
- it 'returns an array of abilites to be prevented when readonly' do
- expect(TestClass.readonly_abilities).to include(*described_class::READONLY_ABILITIES)
- end
- end
-
- describe '.readonly_features' do
- it 'returns an array of features to be prevented when readonly' do
- expect(TestClass.readonly_features).to include(*described_class::READONLY_FEATURES)
- end
- end
-end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 60acacac814..65abb43b6c4 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe GroupPolicy do
+ include AdminModeHelper
include_context 'GroupPolicy context'
context 'public group with no user' do
@@ -1190,12 +1191,28 @@ RSpec.describe GroupPolicy do
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:register_group_runners) }
+ context 'with specific group runner registration disabled' do
+ before do
+ group.runner_registration_enabled = false
+ end
+
+ it { is_expected.to be_allowed(:register_group_runners) }
+ end
+
context 'with group runner registration disabled' do
before do
stub_application_setting(valid_runner_registrars: ['project'])
end
it { is_expected.to be_allowed(:register_group_runners) }
+
+ context 'with specific group runner registration disabled' do
+ before do
+ group.runner_registration_enabled = false
+ end
+
+ it { is_expected.to be_allowed(:register_group_runners) }
+ end
end
end
@@ -1216,6 +1233,14 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_disallowed(:register_group_runners) }
end
+
+ context 'with specific group runner registration disabled' do
+ before do
+ group.runner_registration_enabled = false
+ end
+
+ it { is_expected.to be_disallowed(:register_group_runners) }
+ end
end
context 'with maintainer' do
@@ -1344,4 +1369,32 @@ RSpec.describe GroupPolicy do
subject { described_class.new(current_user, group) }
end
+
+ describe 'read_usage_quotas policy' do
+ context 'reading usage quotas' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:policy) { :read_usage_quotas }
+
+ where(:role, :admin_mode, :allowed) do
+ :owner | nil | true
+ :admin | true | true
+ :admin | false | false
+ :maintainer | nil | false
+ :developer | nil | false
+ :reporter | nil | false
+ :guest | nil | false
+ end
+
+ with_them do
+ let(:current_user) { public_send(role) }
+
+ before do
+ enable_admin_mode!(current_user) if admin_mode
+ end
+
+ it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
+ end
+ end
+ end
end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index c110ca705bd..905ef591b53 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -2,16 +2,19 @@
require 'spec_helper'
-RSpec.describe IssuePolicy do
+RSpec.describe IssuePolicy, feature_category: :team_planning do
include_context 'ProjectPolicyTable context'
include ExternalAuthorizationServiceHelpers
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) }
@@ -197,6 +200,8 @@ RSpec.describe IssuePolicy do
before do
project.add_guest(guest)
project.add_reporter(reporter)
+ project.add_maintainer(maintainer)
+ project.add_owner(owner)
group.add_reporter(reporter_from_group_link)
@@ -305,7 +310,6 @@ RSpec.describe IssuePolicy do
let(:issue) { create(:issue, project: project, author: author) }
let(:visitor) { create(:user) }
- let(:admin) { create(:user, :admin) }
it 'forbids visitors from viewing issues' do
expect(permissions(visitor, issue)).to be_disallowed(:read_issue)
@@ -394,12 +398,15 @@ RSpec.describe IssuePolicy do
expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
end
+
+ it 'allows admins to read confidential issues' do
+ expect(permissions(admin, confidential_issue)).to be_allowed(:read_issue)
+ end
end
context 'with a hidden issue' do
let(:user) { create(:user) }
let(:banned_user) { create(:user, :banned) }
- let(:admin) { create(:user, :admin) }
let(:hidden_issue) { create(:issue, project: project, author: banned_user) }
it 'does not allow non-admin user to read the issue' do
@@ -410,6 +417,37 @@ RSpec.describe IssuePolicy do
expect(permissions(admin, hidden_issue)).to be_allowed(:read_issue)
end
end
+
+ context 'when accounting for notes widget' do
+ let(:policy) { described_class.new(reporter, note) }
+
+ before do
+ widgets_per_type = WorkItems::Type::WIDGETS_FOR_TYPE.dup
+ widgets_per_type[:task] = [::WorkItems::Widgets::Description]
+ stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', widgets_per_type)
+ end
+
+ context 'and notes widget is disabled for task' do
+ let(:task) { create(:work_item, :task, project: project) }
+
+ it 'does not allow accessing notes' do
+ # if notes widget is disabled not even maintainer can access notes
+ expect(permissions(maintainer, task)).to be_disallowed(:create_note, :read_note, :mark_note_as_confidential, :read_internal_note)
+ expect(permissions(admin, task)).to be_disallowed(:create_note, :read_note, :read_internal_note, :mark_note_as_confidential, :set_note_created_at)
+ end
+ end
+
+ context 'and notes widget is enabled for issue' do
+ it 'allows accessing notes' do
+ # with notes widget enabled, even guests can access notes
+ expect(permissions(guest, issue)).to be_allowed(:create_note, :read_note)
+ expect(permissions(guest, issue)).to be_disallowed(:read_internal_note, :mark_note_as_confidential, :set_note_created_at)
+ expect(permissions(reporter, issue)).to be_allowed(:create_note, :read_note, :read_internal_note, :mark_note_as_confidential)
+ expect(permissions(maintainer, issue)).to be_allowed(:create_note, :read_note, :read_internal_note, :mark_note_as_confidential)
+ expect(permissions(owner, issue)).to be_allowed(:create_note, :read_note, :read_internal_note, :mark_note_as_confidential, :set_note_created_at)
+ end
+ end
+ end
end
context 'with external authorization enabled' do
diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb
index 7e1af132b1d..741a0db3009 100644
--- a/spec/policies/merge_request_policy_spec.rb
+++ b/spec/policies/merge_request_policy_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe MergeRequestPolicy do
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:non_team_member) { create(:user) }
+ let_it_be(:bot) { create(:user, :project_bot) }
def permissions(user, merge_request)
described_class.new(user, merge_request)
@@ -72,6 +73,7 @@ RSpec.describe MergeRequestPolicy do
project.add_guest(guest)
project.add_guest(author)
project.add_developer(developer)
+ project.add_developer(bot)
end
context 'when merge request is public' do
@@ -95,6 +97,18 @@ RSpec.describe MergeRequestPolicy do
it do
is_expected.to be_allowed(:approve_merge_request)
end
+
+ it do
+ is_expected.to be_disallowed(:reset_merge_request_approvals)
+ end
+ end
+
+ context 'and the user is a bot' do
+ let(:user) { bot }
+
+ it do
+ is_expected.to be_allowed(:reset_merge_request_approvals)
+ end
end
end
end
@@ -123,6 +137,14 @@ RSpec.describe MergeRequestPolicy do
it_behaves_like 'a denied user'
end
+
+ describe 'a bot' do
+ let(:subject) { permissions(bot, merge_request) }
+
+ it do
+ is_expected.to be_disallowed(:reset_merge_request_approvals)
+ end
+ end
end
context 'when merge requests are private' do
@@ -144,6 +166,14 @@ RSpec.describe MergeRequestPolicy do
it_behaves_like 'a user with full access'
end
+
+ describe 'a bot' do
+ let(:subject) { permissions(bot, merge_request) }
+
+ it do
+ is_expected.to be_allowed(:reset_merge_request_approvals)
+ end
+ end
end
context 'when merge request is unlocked' do
@@ -214,6 +244,7 @@ RSpec.describe MergeRequestPolicy do
group.add_guest(author)
group.add_reporter(reporter)
group.add_developer(developer)
+ group.add_developer(bot)
end
context 'when project is public' do
@@ -222,9 +253,25 @@ RSpec.describe MergeRequestPolicy do
describe 'the merge request author' do
subject { permissions(author, merge_request) }
- specify do
+ it do
is_expected.to be_allowed(:approve_merge_request)
end
+
+ it do
+ is_expected.to be_disallowed(:reset_merge_request_approvals)
+ end
+ end
+
+ describe 'a bot' do
+ subject { permissions(bot, merge_request) }
+
+ it do
+ is_expected.to be_allowed(:approve_merge_request)
+ end
+
+ it do
+ is_expected.to be_allowed(:reset_merge_request_approvals)
+ end
end
context 'and merge requests are private' do
@@ -250,6 +297,14 @@ RSpec.describe MergeRequestPolicy do
it_behaves_like 'a user with full access'
end
+
+ describe 'a bot' do
+ let(:subject) { permissions(bot, merge_request) }
+
+ it do
+ is_expected.to be_allowed(:reset_merge_request_approvals)
+ end
+ end
end
end
@@ -273,6 +328,14 @@ RSpec.describe MergeRequestPolicy do
it_behaves_like 'a user with full access'
end
+
+ describe 'a bot' do
+ let(:subject) { permissions(bot, merge_request) }
+
+ it do
+ is_expected.to be_allowed(:reset_merge_request_approvals)
+ end
+ end
end
end
@@ -297,11 +360,28 @@ RSpec.describe MergeRequestPolicy do
group_access: Gitlab::Access::DEVELOPER)
group.add_guest(non_team_member)
+ group.add_guest(bot)
end
- specify do
+ it do
is_expected.to be_allowed(:approve_merge_request)
end
+
+ it do
+ is_expected.to be_disallowed(:reset_merge_request_approvals)
+ end
+
+ context 'and the user is a bot' do
+ let(:user) { bot }
+
+ it do
+ is_expected.to be_allowed(:approve_merge_request)
+ end
+
+ it do
+ is_expected.to be_allowed(:reset_merge_request_approvals)
+ end
+ end
end
end
@@ -313,9 +393,25 @@ RSpec.describe MergeRequestPolicy do
subject { permissions(non_team_member, merge_request) }
- specify do
+ it do
is_expected.not_to be_allowed(:approve_merge_request)
end
+
+ it do
+ is_expected.not_to be_allowed(:reset_merge_request_approvals)
+ end
+
+ context 'and the user is a bot' do
+ subject { permissions(bot, merge_request) }
+
+ it do
+ is_expected.not_to be_allowed(:approve_merge_request)
+ end
+
+ it do
+ is_expected.not_to be_allowed(:reset_merge_request_approvals)
+ end
+ end
end
context 'when merge requests are disabled' do
diff --git a/spec/policies/namespaces/user_namespace_policy_spec.rb b/spec/policies/namespaces/user_namespace_policy_spec.rb
index 42d27d0f3d6..bb821490e30 100644
--- a/spec/policies/namespaces/user_namespace_policy_spec.rb
+++ b/spec/policies/namespaces/user_namespace_policy_spec.rb
@@ -35,6 +35,13 @@ RSpec.describe Namespaces::UserNamespacePolicy do
it { is_expected.to be_disallowed(:create_projects) }
it { is_expected.to be_disallowed(:transfer_projects) }
end
+
+ context 'bot user' do
+ let(:owner) { create(:user, :project_bot) }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ it { is_expected.to be_disallowed(:transfer_projects) }
+ end
end
context 'admin' do
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index 6a261b4ff5b..dcfc398806a 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe NotePolicy do
+RSpec.describe NotePolicy, feature_category: :team_planning do
describe '#rules', :aggregate_failures do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
@@ -255,6 +255,31 @@ RSpec.describe NotePolicy do
it_behaves_like 'user can read the note'
end
+
+ context 'when notes widget is disabled for task' do
+ let(:policy) { described_class.new(developer, note) }
+
+ before do
+ widgets_per_type = WorkItems::Type::WIDGETS_FOR_TYPE.dup
+ widgets_per_type[:task] = [::WorkItems::Widgets::Description]
+ stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', widgets_per_type)
+ end
+
+ context 'when noteable is task' do
+ let(:noteable) { create(:work_item, :task, project: project) }
+ let(:note) { create(:note, system: true, noteable: noteable, author: user, project: project) }
+
+ it_behaves_like 'user cannot read or act on the note'
+ end
+
+ context 'when noteable is issue' do
+ let(:noteable) { create(:work_item, :issue, project: project) }
+ let(:note) { create(:note, system: true, noteable: noteable, author: user, project: project) }
+
+ it_behaves_like 'user can read the note'
+ it_behaves_like 'user can act on the note'
+ end
+ end
end
context 'when it is a system note referencing a confidential issue' do
@@ -313,7 +338,7 @@ RSpec.describe NotePolicy do
end
it 'does not allow guests to read confidential notes and replies' do
- expect(permissions(guest, confidential_note)).to be_disallowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji, :mark_note_as_confidential)
+ expect(permissions(guest, confidential_note)).to be_disallowed(:read_note, :read_internal_note, :admin_note, :reposition_note, :resolve_note, :award_emoji, :mark_note_as_confidential)
end
it 'allows reporter to read all notes but not resolve and admin them' do
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 973ed66b8d8..9b2d10283f1 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -1965,87 +1965,6 @@ RSpec.describe ProjectPolicy do
it_behaves_like 'Self-managed Core resource access tokens'
- describe 'operations feature' do
- using RSpec::Parameterized::TableSyntax
-
- let(:guest_permissions) { [:read_environment, :read_deployment] }
-
- let(:developer_permissions) do
- guest_permissions + [
- :read_feature_flag, :read_sentry_issue, :read_alert_management_alert, :read_terraform_state,
- :metrics_dashboard, :read_pod_logs, :read_prometheus, :create_feature_flag,
- :create_environment, :create_deployment, :update_feature_flag, :update_environment,
- :update_sentry_issue, :update_alert_management_alert, :update_deployment,
- :destroy_feature_flag, :destroy_environment, :admin_feature_flag
- ]
- end
-
- let(:maintainer_permissions) do
- developer_permissions + [
- :read_cluster, :create_cluster, :update_cluster, :admin_environment,
- :admin_cluster, :admin_terraform_state, :admin_deployment
- ]
- end
-
- before do
- stub_feature_flags(split_operations_visibility_permissions: false)
- 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 | false
- :private | ProjectFeature::ENABLED | :anonymous | false
- :private | ProjectFeature::PRIVATE | :maintainer | true
- :private | ProjectFeature::PRIVATE | :developer | true
- :private | ProjectFeature::PRIVATE | :guest | false
- :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
-
- with_them do
- let(:current_user) { user_subject(role) }
- let(:project) { project_subject(project_visibility) }
-
- it 'allows/disallows the abilities based on the operation feature access level' do
- project.project_feature.update!(operations_access_level: access_level)
-
- if allowed
- expect_allowed(*permissions_abilities(role))
- else
- expect_disallowed(*permissions_abilities(role))
- end
- end
- end
- end
-
describe 'environments feature' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index 88dafb7ea1f..f8cba8e9203 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe BlobPresenter do
end
describe '#permalink_path' do
- it { expect(presenter.permalink_path).to eq("/#{project.full_path}/-/blob/#{project.repository.commit.sha}/files/ruby/regex.rb") }
+ it { expect(presenter.permalink_path).to eq("/#{project.full_path}/-/blob/#{project.repository.commit(blob.commit_id).sha}/files/ruby/regex.rb") }
end
context 'environment has been deployed' do
diff --git a/spec/presenters/ci/freeze_period_presenter_spec.rb b/spec/presenters/ci/freeze_period_presenter_spec.rb
new file mode 100644
index 00000000000..e9959540b8d
--- /dev/null
+++ b/spec/presenters/ci/freeze_period_presenter_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::FreezePeriodPresenter, feature_category: :release_orchestration do
+ let_it_be(:project) { build_stubbed(:project) }
+
+ let(:presenter) { described_class.new(freeze_period) }
+
+ describe '#start_time' do
+ let(:freeze_period) { build_stubbed(:ci_freeze_period, project: project) }
+
+ context 'when active' do
+ # Default freeze period factory is on a weekend, so let's travel in time to a Saturday!
+ let(:time) { Time.utc(2022, 12, 3, 6) }
+ let(:previous_start) { Time.utc(2022, 12, 2, 23) }
+
+ it 'returns the previous time of the freeze period start' do
+ travel_to(time) do
+ expect(presenter.start_time).to eq(previous_start)
+ end
+ end
+ end
+
+ context 'when inactive' do
+ # Default freeze period factory is on a weekend, so we travel back a couple of days earlier.
+ let(:time) { Time.utc(2022, 11, 30, 6) }
+ let(:next_start) { Time.utc(2022, 12, 2, 23) }
+
+ it 'returns the next time of the freeze period start' do
+ travel_to(time) do
+ expect(presenter.start_time).to eq(next_start)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/presenters/group_member_presenter_spec.rb b/spec/presenters/group_member_presenter_spec.rb
index 352f81356e0..25992871160 100644
--- a/spec/presenters/group_member_presenter_spec.rb
+++ b/spec/presenters/group_member_presenter_spec.rb
@@ -54,6 +54,24 @@ RSpec.describe GroupMemberPresenter do
end
end
+ describe '#last_owner?' do
+ context 'when member is the last owner of the group' do
+ before do
+ allow(group_member).to receive(:last_owner_of_the_group?).and_return(true)
+ end
+
+ it { expect(presenter.last_owner?).to eq(true) }
+ end
+
+ context 'when member is not the last owner of the group' do
+ before do
+ allow(group_member).to receive(:last_owner_of_the_group?).and_return(false)
+ end
+
+ it { expect(presenter.last_owner?).to eq(false) }
+ end
+ end
+
describe '#can_update?' do
context 'when user can update_group_member' do
before do
diff --git a/spec/presenters/member_presenter_spec.rb b/spec/presenters/member_presenter_spec.rb
new file mode 100644
index 00000000000..65e23d20051
--- /dev/null
+++ b/spec/presenters/member_presenter_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MemberPresenter, feature_category: :subgroups do
+ let_it_be(:member) { build(:group_member) }
+ let(:presenter) { described_class.new(member) }
+
+ describe '#last_owner?' do
+ it 'raises `NotImplementedError`' do
+ expect { presenter.last_owner? }.to raise_error(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/presenters/packages/pypi/simple_package_versions_presenter_spec.rb b/spec/presenters/packages/pypi/simple_package_versions_presenter_spec.rb
index c966b1fc8e1..50a5cb631f9 100644
--- a/spec/presenters/packages/pypi/simple_package_versions_presenter_spec.rb
+++ b/spec/presenters/packages/pypi/simple_package_versions_presenter_spec.rb
@@ -36,6 +36,14 @@ RSpec.describe ::Packages::Pypi::SimplePackageVersionsPresenter, :aggregate_fail
it { is_expected.to include expected_link }
end
+
+ it 'avoids N+1 database queries' do
+ control = ActiveRecord::QueryRecorder.new { subject }
+
+ create(:pypi_package, project: project, name: package_name)
+
+ expect { described_class.new(project.packages, project_or_group).body }.not_to exceed_query_limit(control)
+ end
end
context 'for project' do
diff --git a/spec/presenters/project_member_presenter_spec.rb b/spec/presenters/project_member_presenter_spec.rb
index 1cfc8cfb53b..28afb78cdce 100644
--- a/spec/presenters/project_member_presenter_spec.rb
+++ b/spec/presenters/project_member_presenter_spec.rb
@@ -54,6 +54,24 @@ RSpec.describe ProjectMemberPresenter do
end
end
+ describe '#last_owner?' do
+ context 'when member is the holder of the personal namespace' do
+ before do
+ allow(project_member).to receive(:holder_of_the_personal_namespace?).and_return(true)
+ end
+
+ it { expect(presenter.last_owner?).to eq(true) }
+ end
+
+ context 'when member is not the holder of the personal namespace' do
+ before do
+ allow(project_member).to receive(:holder_of_the_personal_namespace?).and_return(false)
+ end
+
+ it { expect(presenter.last_owner?).to eq(false) }
+ end
+ end
+
describe '#can_update?' do
context 'when user is NOT attempting to update an Owner' do
before do
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index c32cc87afbb..4c2b87f34a1 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -76,21 +76,21 @@ RSpec.describe ProjectPresenter do
let_it_be(:project) { create(:project, :public, :repository) }
it 'returns files and readme if user has repository access' do
- allow(presenter).to receive(:can?).with(nil, :download_code, project).and_return(true)
+ allow(presenter).to receive(:can?).with(nil, :read_code, project).and_return(true)
expect(presenter.default_view).to eq('files')
end
it 'returns wiki if user does not have repository access and can read wiki, which exists' do
allow(project).to receive(:wiki_repository_exists?).and_return(true)
- allow(presenter).to receive(:can?).with(nil, :download_code, project).and_return(false)
+ allow(presenter).to receive(:can?).with(nil, :read_code, project).and_return(false)
allow(presenter).to receive(:can?).with(nil, :read_wiki, project).and_return(true)
expect(presenter.default_view).to eq('wiki')
end
it 'returns activity if user does not have repository or wiki access' do
- allow(presenter).to receive(:can?).with(nil, :download_code, project).and_return(false)
+ allow(presenter).to receive(:can?).with(nil, :read_code, project).and_return(false)
allow(presenter).to receive(:can?).with(nil, :read_issue, project).and_return(false)
allow(presenter).to receive(:can?).with(nil, :read_wiki, project).and_return(false)
@@ -117,7 +117,7 @@ RSpec.describe ProjectPresenter do
context 'when the user is allowed to see the code' do
it 'returns the project view' do
- allow(presenter).to receive(:can?).with(user, :download_code, project).and_return(true)
+ allow(presenter).to receive(:can?).with(user, :read_code, project).and_return(true)
expect(presenter.default_view).to eq('readme')
end
@@ -126,7 +126,7 @@ RSpec.describe ProjectPresenter do
context 'with wikis enabled and the right policy for the user' do
before do
project.project_feature.update_attribute(:issues_access_level, 0)
- allow(presenter).to receive(:can?).with(user, :download_code, project).and_return(false)
+ allow(presenter).to receive(:can?).with(user, :read_code, project).and_return(false)
end
it 'returns wiki if the user has the right policy and the wiki exists' do
@@ -146,7 +146,7 @@ RSpec.describe ProjectPresenter do
context 'with issues as a feature available' do
it 'return issues' do
- allow(presenter).to receive(:can?).with(user, :download_code, project).and_return(false)
+ allow(presenter).to receive(:can?).with(user, :read_code, project).and_return(false)
allow(presenter).to receive(:can?).with(user, :read_issue, project).and_return(true)
allow(presenter).to receive(:can?).with(user, :read_wiki, project).and_return(false)
@@ -157,7 +157,7 @@ RSpec.describe ProjectPresenter do
context 'with no activity, no wikies and no issues' do
it 'returns activity as default' do
project.project_feature.update_attribute(:issues_access_level, 0)
- allow(presenter).to receive(:can?).with(user, :download_code, project).and_return(false)
+ allow(presenter).to receive(:can?).with(user, :read_code, project).and_return(false)
allow(presenter).to receive(:can?).with(user, :read_wiki, project).and_return(false)
allow(presenter).to receive(:can?).with(user, :read_issue, project).and_return(false)
@@ -623,14 +623,6 @@ RSpec.describe ProjectPresenter do
context 'empty repo' do
let(:project) { create(:project, :stubbed_repository) }
- context 'for a guest user' do
- it 'orders the items correctly' do
- expect(empty_repo_statistics_buttons.map(&:label)).to start_with(
- a_string_including('No license')
- )
- end
- end
-
it 'includes a button to configure integrations for maintainers' do
project.add_maintainer(user)
diff --git a/spec/presenters/projects/security/configuration_presenter_spec.rb b/spec/presenters/projects/security/configuration_presenter_spec.rb
index ca7f96b567d..4fe459a798a 100644
--- a/spec/presenters/projects/security/configuration_presenter_spec.rb
+++ b/spec/presenters/projects/security/configuration_presenter_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
let(:presenter) { described_class.new(project, current_user: current_user) }
before do
- stub_licensed_features(licensed_scan_types.to_h { |type| [type, true] })
+ stub_licensed_features(licensed_scan_types.index_with { true })
end
describe '#to_html_data_attribute' do
diff --git a/spec/presenters/search_service_presenter_spec.rb b/spec/presenters/search_service_presenter_spec.rb
index af9fee8cfd9..a235f954366 100644
--- a/spec/presenters/search_service_presenter_spec.rb
+++ b/spec/presenters/search_service_presenter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SearchServicePresenter do
+RSpec.describe SearchServicePresenter, feature_category: :global_search do
let(:user) { create(:user) }
let(:search) { '' }
let(:search_service) { SearchService.new(user, search: search, scope: scope) }
@@ -51,4 +51,10 @@ RSpec.describe SearchServicePresenter do
it { expect(presenter.show_results_status?).to eq(result) }
end
end
+
+ describe '#advanced_search_enabled?' do
+ let(:scope) { nil }
+
+ it { expect(presenter.advanced_search_enabled?).to eq(false) }
+ end
end
diff --git a/spec/requests/abuse_reports_controller_spec.rb b/spec/requests/abuse_reports_controller_spec.rb
index 94c80ccb89a..510855d95e0 100644
--- a/spec/requests/abuse_reports_controller_spec.rb
+++ b/spec/requests/abuse_reports_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AbuseReportsController do
+RSpec.describe AbuseReportsController, feature_category: :users do
let(:reporter) { create(:user) }
let(:user) { create(:user) }
let(:attrs) do
diff --git a/spec/requests/admin/applications_controller_spec.rb b/spec/requests/admin/applications_controller_spec.rb
index 03553757080..c83137ebbce 100644
--- a/spec/requests/admin/applications_controller_spec.rb
+++ b/spec/requests/admin/applications_controller_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Admin::ApplicationsController, :enable_admin_mode do
+RSpec.describe Admin::ApplicationsController, :enable_admin_mode,
+feature_category: :authentication_and_authorization do
let_it_be(:admin) { create(:admin) }
let_it_be(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) }
let_it_be(:show_path) { admin_application_path(application) }
diff --git a/spec/requests/admin/background_migrations_controller_spec.rb b/spec/requests/admin/background_migrations_controller_spec.rb
index fe2a2470511..db3e2fa0df6 100644
--- a/spec/requests/admin/background_migrations_controller_spec.rb
+++ b/spec/requests/admin/background_migrations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::BackgroundMigrationsController, :enable_admin_mode do
+RSpec.describe Admin::BackgroundMigrationsController, :enable_admin_mode, feature_category: :database do
let(:admin) { create(:admin) }
before do
diff --git a/spec/requests/admin/batched_jobs_controller_spec.rb b/spec/requests/admin/batched_jobs_controller_spec.rb
index fb51b3bce88..8060b19b8b3 100644
--- a/spec/requests/admin/batched_jobs_controller_spec.rb
+++ b/spec/requests/admin/batched_jobs_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::BatchedJobsController, :enable_admin_mode do
+RSpec.describe Admin::BatchedJobsController, :enable_admin_mode, feature_category: :database do
let(:admin) { create(:admin) }
before do
diff --git a/spec/requests/admin/broadcast_messages_controller_spec.rb b/spec/requests/admin/broadcast_messages_controller_spec.rb
index eb29092845c..69b84d6d795 100644
--- a/spec/requests/admin/broadcast_messages_controller_spec.rb
+++ b/spec/requests/admin/broadcast_messages_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode do
+RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode, feature_category: :onboarding do
let(:broadcast_message) { build(:broadcast_message) }
let(:broadcast_message_params) { broadcast_message.as_json(root: true, only: [:message, :starts_at, :ends_at]) }
diff --git a/spec/requests/admin/clusters/integrations_controller_spec.rb b/spec/requests/admin/clusters/integrations_controller_spec.rb
index ee1c1d5aad4..d5e3f03627a 100644
--- a/spec/requests/admin/clusters/integrations_controller_spec.rb
+++ b/spec/requests/admin/clusters/integrations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::Clusters::IntegrationsController, :enable_admin_mode do
+RSpec.describe Admin::Clusters::IntegrationsController, :enable_admin_mode, feature_category: :integrations do
include AccessMatchersForController
shared_examples 'a secure endpoint' do
diff --git a/spec/requests/admin/hook_logs_controller_spec.rb b/spec/requests/admin/hook_logs_controller_spec.rb
index f8d3381c052..fa9f317dbba 100644
--- a/spec/requests/admin/hook_logs_controller_spec.rb
+++ b/spec/requests/admin/hook_logs_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::HookLogsController, :enable_admin_mode do
+RSpec.describe Admin::HookLogsController, :enable_admin_mode, feature_category: :integrations do
let_it_be(:user) { create(:admin) }
let_it_be_with_refind(:web_hook) { create(:system_hook) }
let_it_be_with_refind(:web_hook_log) { create(:web_hook_log, web_hook: web_hook) }
diff --git a/spec/requests/admin/impersonation_tokens_controller_spec.rb b/spec/requests/admin/impersonation_tokens_controller_spec.rb
index ee0e12ad0c0..15212db0e77 100644
--- a/spec/requests/admin/impersonation_tokens_controller_spec.rb
+++ b/spec/requests/admin/impersonation_tokens_controller_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Admin::ImpersonationTokensController, :enable_admin_mode do
+RSpec.describe Admin::ImpersonationTokensController, :enable_admin_mode,
+feature_category: :authentication_and_authorization do
let(:admin) { create(:admin) }
let!(:user) { create(:user) }
diff --git a/spec/requests/admin/integrations_controller_spec.rb b/spec/requests/admin/integrations_controller_spec.rb
index 128aada0975..efd0e3d91ee 100644
--- a/spec/requests/admin/integrations_controller_spec.rb
+++ b/spec/requests/admin/integrations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::IntegrationsController, :enable_admin_mode do
+RSpec.describe Admin::IntegrationsController, :enable_admin_mode, feature_category: :integrations do
let_it_be(:admin) { create(:admin) }
before do
diff --git a/spec/requests/admin/version_check_controller_spec.rb b/spec/requests/admin/version_check_controller_spec.rb
index 7e2f33d5bc5..47221bf37e5 100644
--- a/spec/requests/admin/version_check_controller_spec.rb
+++ b/spec/requests/admin/version_check_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::VersionCheckController, :enable_admin_mode do
+RSpec.describe Admin::VersionCheckController, :enable_admin_mode, feature_category: :not_owned do
let(:admin) { create(:admin) }
before do
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 2af6c438fc9..8c14ead9e42 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::AccessRequests do
+RSpec.describe API::AccessRequests, feature_category: :authentication_and_authorization do
let_it_be(:maintainer) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:access_requester) { create(:user) }
diff --git a/spec/requests/api/admin/batched_background_migrations_spec.rb b/spec/requests/api/admin/batched_background_migrations_spec.rb
index 3b396a91d3e..9712777d261 100644
--- a/spec/requests/api/admin/batched_background_migrations_spec.rb
+++ b/spec/requests/api/admin/batched_background_migrations_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Admin::BatchedBackgroundMigrations do
+RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :database do
let(:admin) { create(:admin) }
let(:unauthorized_user) { create(:user) }
diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb
index 7b3224f58c5..7b510f74fd4 100644
--- a/spec/requests/api/admin/instance_clusters_spec.rb
+++ b/spec/requests/api/admin/instance_clusters_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::API::Admin::InstanceClusters do
+RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_management do
include KubernetesHelpers
let_it_be(:regular_user) { create(:user) }
diff --git a/spec/requests/api/admin/plan_limits_spec.rb b/spec/requests/api/admin/plan_limits_spec.rb
index 74ea3b0973f..2de7a66d803 100644
--- a/spec/requests/api/admin/plan_limits_spec.rb
+++ b/spec/requests/api/admin/plan_limits_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
+RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owned do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:plan) { create(:plan, name: 'default') }
@@ -40,6 +40,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
expect(json_response['pypi_max_file_size']).to eq(Plan.default.actual_limits.pypi_max_file_size)
expect(json_response['terraform_module_max_file_size']).to eq(Plan.default.actual_limits.terraform_module_max_file_size)
expect(json_response['storage_size_limit']).to eq(Plan.default.actual_limits.storage_size_limit)
+ expect(json_response['pipeline_hierarchy_size']).to eq(Plan.default.actual_limits.pipeline_hierarchy_size)
end
end
@@ -70,6 +71,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
expect(json_response['pypi_max_file_size']).to eq(Plan.default.actual_limits.pypi_max_file_size)
expect(json_response['terraform_module_max_file_size']).to eq(Plan.default.actual_limits.terraform_module_max_file_size)
expect(json_response['storage_size_limit']).to eq(Plan.default.actual_limits.storage_size_limit)
+ expect(json_response['pipeline_hierarchy_size']).to eq(Plan.default.actual_limits.pipeline_hierarchy_size)
end
end
@@ -118,7 +120,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
'nuget_max_file_size': 50,
'pypi_max_file_size': 60,
'terraform_module_max_file_size': 70,
- 'storage_size_limit': 80
+ 'storage_size_limit': 80,
+ 'pipeline_hierarchy_size': 250
}
expect(response).to have_gitlab_http_status(:ok)
@@ -140,6 +143,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
expect(json_response['pypi_max_file_size']).to eq(60)
expect(json_response['terraform_module_max_file_size']).to eq(70)
expect(json_response['storage_size_limit']).to eq(80)
+ expect(json_response['pipeline_hierarchy_size']).to eq(250)
end
it 'updates single plan limits' do
@@ -183,7 +187,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
'nuget_max_file_size': 'e',
'pypi_max_file_size': 'f',
'terraform_module_max_file_size': 'g',
- 'storage_size_limit': 'j'
+ 'storage_size_limit': 'j',
+ 'pipeline_hierarchy_size': 'r'
}
expect(response).to have_gitlab_http_status(:bad_request)
@@ -204,7 +209,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
'nuget_max_file_size is invalid',
'pypi_max_file_size is invalid',
'terraform_module_max_file_size is invalid',
- 'storage_size_limit is invalid'
+ 'storage_size_limit is invalid',
+ 'pipeline_hierarchy_size is invalid'
)
end
end
diff --git a/spec/requests/api/admin/sidekiq_spec.rb b/spec/requests/api/admin/sidekiq_spec.rb
index 1e626c90e7e..0b456721d4f 100644
--- a/spec/requests/api/admin/sidekiq_spec.rb
+++ b/spec/requests/api/admin/sidekiq_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues do
+RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category: :not_owned do
let_it_be(:admin) { create(:admin) }
describe 'DELETE /admin/sidekiq/queues/:queue_name' do
diff --git a/spec/requests/api/alert_management_alerts_spec.rb b/spec/requests/api/alert_management_alerts_spec.rb
index 680a3883387..8dd0c46eab6 100644
--- a/spec/requests/api/alert_management_alerts_spec.rb
+++ b/spec/requests/api/alert_management_alerts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::AlertManagementAlerts do
+RSpec.describe API::AlertManagementAlerts, feature_category: :incident_management do
let_it_be(:creator) { create(:user) }
let_it_be(:project) do
create(:project, :public, creator_id: creator.id, namespace: creator.namespace)
diff --git a/spec/requests/api/api_guard/admin_mode_middleware_spec.rb b/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
index ba7a01a2cd9..21f3691c20b 100644
--- a/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
+++ b/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::APIGuard::AdminModeMiddleware, :request_store do
+RSpec.describe API::APIGuard::AdminModeMiddleware, :request_store, feature_category: :not_owned do
let(:user) { create(:admin) }
it 'is loaded' do
diff --git a/spec/requests/api/api_guard/response_coercer_middleware_spec.rb b/spec/requests/api/api_guard/response_coercer_middleware_spec.rb
index 6f3f97fe846..77498c2e2b3 100644
--- a/spec/requests/api/api_guard/response_coercer_middleware_spec.rb
+++ b/spec/requests/api/api_guard/response_coercer_middleware_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::APIGuard::ResponseCoercerMiddleware do
+RSpec.describe API::APIGuard::ResponseCoercerMiddleware, feature_category: :not_owned do
using RSpec::Parameterized::TableSyntax
it 'is loaded' do
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index 260f7cbc226..9cf9c313f11 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::API do
+RSpec.describe API::API, feature_category: :authentication_and_authorization do
include GroupAPIHelpers
describe 'Record user last activity in after hook' do
diff --git a/spec/requests/api/appearance_spec.rb b/spec/requests/api/appearance_spec.rb
index 69176e18d2e..84d5b091b8d 100644
--- a/spec/requests/api/appearance_spec.rb
+++ b/spec/requests/api/appearance_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Appearance, 'Appearance' do
+RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
@@ -33,6 +33,7 @@ RSpec.describe API::Appearance, 'Appearance' do
expect(json_response['new_project_guidelines']).to eq('')
expect(json_response['profile_image_guidelines']).to eq('')
expect(json_response['title']).to eq('')
+ expect(json_response['short_title']).to eq('')
end
end
end
@@ -51,6 +52,7 @@ RSpec.describe API::Appearance, 'Appearance' do
it "allows updating the settings" do
put api("/application/appearance", admin), params: {
title: "GitLab Test Instance",
+ short_title: "GitLab",
description: "gitlab-test.example.com",
new_project_guidelines: "Please read the FAQs for help.",
profile_image_guidelines: "Custom profile image guidelines"
@@ -70,6 +72,7 @@ RSpec.describe API::Appearance, 'Appearance' do
expect(json_response['new_project_guidelines']).to eq('Please read the FAQs for help.')
expect(json_response['profile_image_guidelines']).to eq('Custom profile image guidelines')
expect(json_response['title']).to eq('GitLab Test Instance')
+ expect(json_response['short_title']).to eq('GitLab')
end
end
diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb
index 022451553ee..e238a1fb554 100644
--- a/spec/requests/api/applications_spec.rb
+++ b/spec/requests/api/applications_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Applications, :api do
+RSpec.describe API::Applications, :api, feature_category: :authentication_and_authorization do
let(:admin_user) { create(:user, admin: true) }
let(:user) { create(:user, admin: false) }
let(:scopes) { 'api' }
diff --git a/spec/requests/api/avatar_spec.rb b/spec/requests/api/avatar_spec.rb
index 656a086e550..8affbe6ec2b 100644
--- a/spec/requests/api/avatar_spec.rb
+++ b/spec/requests/api/avatar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Avatar do
+RSpec.describe API::Avatar, feature_category: :users do
let(:gravatar_service) { double('GravatarService') }
describe 'GET /avatar' do
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index bb563f93bfe..87dc06b7d15 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::AwardEmoji do
+RSpec.describe API::AwardEmoji, feature_category: :not_owned do
let_it_be_with_reload(:project) { create(:project, :private) }
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, project: project) }
diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb
index d8a345a79b0..6c6a7cc7cc6 100644
--- a/spec/requests/api/badges_spec.rb
+++ b/spec/requests/api/badges_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Badges do
+RSpec.describe API::Badges, feature_category: :projects do
let(:maintainer) { create(:user, username: 'maintainer_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 4d7256a1f03..69804c2c4a4 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Boards do
+RSpec.describe API::Boards, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:non_member) { create(:user) }
let_it_be(:guest) { create(:user) }
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 750b9a39e15..eba1a06b5e4 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Branches do
+RSpec.describe API::Branches, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let(:project) { create(:project, :repository, creator: user, path: 'my.project', create_branch: 'ends-with.txt') }
@@ -344,6 +344,18 @@ RSpec.describe API::Branches do
end
end
+ context 'when branch is ambiguous' do
+ let(:branch_name) { 'prefix' }
+
+ before do
+ project.repository.create_branch('prefix/branch')
+ end
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
+
context 'when repository does not exist' do
it_behaves_like '404 response' do
let(:project) { create(:project, creator: user) }
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index 76412c80f4c..5cbb7dbfa12 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::BroadcastMessages do
+RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:message) { create(:broadcast_message) }
diff --git a/spec/requests/api/bulk_imports_spec.rb b/spec/requests/api/bulk_imports_spec.rb
index ad57a370fc5..13f079c69e7 100644
--- a/spec/requests/api/bulk_imports_spec.rb
+++ b/spec/requests/api/bulk_imports_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::BulkImports do
+RSpec.describe API::BulkImports, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:import_1) { create(:bulk_import, user: user) }
let_it_be(:import_2) { create(:bulk_import, user: user) }
@@ -50,6 +50,9 @@ RSpec.describe API::BulkImports do
.to receive(:instance_version)
.and_return(
Gitlab::VersionInfo.new(::BulkImport::MIN_MAJOR_VERSION, ::BulkImport::MIN_MINOR_VERSION_FOR_PROJECT))
+ allow(instance)
+ .to receive(:instance_enterprise)
+ .and_return(false)
end
end
diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb
index da9eb6b2216..a4a38179d11 100644
--- a/spec/requests/api/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/ci/job_artifacts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::JobArtifacts do
+RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
include HttpBasicAuthHelpers
include DependencyProxyHelpers
diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb
index c1b7461f444..4e348ae64b6 100644
--- a/spec/requests/api/ci/jobs_spec.rb
+++ b/spec/requests/api/ci/jobs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Jobs do
+RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
include HttpBasicAuthHelpers
include DependencyProxyHelpers
@@ -190,15 +190,56 @@ RSpec.describe API::Ci::Jobs do
describe 'GET /job/allowed_agents' do
let_it_be(:group) { create(:group) }
- let_it_be(:group_agent) { create(:cluster_agent, project: create(:project, group: group)) }
- let_it_be(:group_authorization) { create(:agent_group_authorization, agent: group_agent, group: group) }
- let_it_be(:project_agent) { create(:cluster_agent, project: project) }
- before(:all) do
- project.update!(group: group_authorization.group)
+ # create a different project for the group agents to reference
+ # otherwise the AgentAuthorizationsFinder will pick up the project.cluster_agents' implicit authorizations
+ let_it_be(:other_project) { create(:project, group: group) }
+
+ let_it_be(:agent_authorizations_without_env) do
+ [
+ create(:agent_group_authorization, agent: create(:cluster_agent, project: other_project), group: group),
+ create(:agent_project_authorization, agent: create(:cluster_agent, project: project), project: project),
+ Clusters::Agents::ImplicitAuthorization.new(agent: create(:cluster_agent, project: project))
+ ]
+ end
+
+ let_it_be(:agent_authorizations_with_review_and_production_env) do
+ [
+ create(
+ :agent_group_authorization,
+ agent: create(:cluster_agent, project: other_project),
+ group: group,
+ environments: ['production', 'review/*']
+ ),
+ create(
+ :agent_project_authorization,
+ agent: create(:cluster_agent, project: project),
+ project: project,
+ environments: ['production', 'review/*']
+ )
+ ]
+ end
+
+ let_it_be(:agent_authorizations_with_staging_env) do
+ [
+ create(
+ :agent_group_authorization,
+ agent: create(:cluster_agent, project: other_project),
+ group: group,
+ environments: ['staging']
+ ),
+ create(
+ :agent_project_authorization,
+ agent: create(:cluster_agent, project: project),
+ project: project,
+ environments: ['staging']
+ )
+ ]
end
- let(:implicit_authorization) { Clusters::Agents::ImplicitAuthorization.new(agent: project_agent) }
+ before(:all) do
+ project.update!(group: group)
+ end
let(:headers) { { API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => job.token } }
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user, status: job_status) }
@@ -215,30 +256,46 @@ RSpec.describe API::Ci::Jobs do
context 'when token is valid and user is authorized' do
shared_examples_for 'valid allowed_agents request' do
- it 'returns agent info', :aggregate_failures do
+ it 'returns the job info', :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.dig('job', 'id')).to eq(job.id)
expect(json_response.dig('pipeline', 'id')).to eq(job.pipeline_id)
expect(json_response.dig('project', 'id')).to eq(job.project_id)
- expect(json_response.dig('project', 'groups')).to match_array([{ 'id' => group_authorization.group.id }])
+ 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).not_to include('environment')
- expect(json_response['allowed_agents']).to match_array(
- [
- {
- 'id' => implicit_authorization.agent_id,
- 'config_project' => hash_including('id' => implicit_authorization.agent.project_id),
- 'configuration' => implicit_authorization.config
- },
- {
- 'id' => group_authorization.agent_id,
- 'config_project' => hash_including('id' => group_authorization.agent.project_id),
- 'configuration' => group_authorization.config
- }
- ])
+ end
+
+ it 'returns the agents allowed for the job' do
+ expected_allowed_agents = agent_authorizations_without_env.map do |agent_auth|
+ {
+ 'id' => agent_auth.agent_id,
+ 'config_project' => hash_including('id' => agent_auth.agent.project_id),
+ 'configuration' => agent_auth.config
+ }
+ end
+
+ expect(json_response['allowed_agents']).to match_array expected_allowed_agents
+ end
+ end
+
+ shared_examples_for 'valid allowed_agents request for a job with environment' do
+ it 'return the agents configured for the given environment' do
+ expected_allowed_agents = (
+ agent_authorizations_without_env +
+ agent_authorizations_with_review_and_production_env
+ ).map do |agent_auth|
+ {
+ 'id' => agent_auth.agent_id,
+ 'config_project' => hash_including('id' => agent_auth.agent.project_id),
+ 'configuration' => agent_auth.config
+ }
+ end
+
+ expect(json_response['allowed_agents']).to match_array(expected_allowed_agents)
end
end
@@ -254,21 +311,25 @@ RSpec.describe API::Ci::Jobs do
it 'includes environment tier' do
expect(json_response.dig('environment', 'tier')).to eq('production')
end
+
+ it_behaves_like 'valid allowed_agents request for a job with environment'
end
context 'when non-deployment environment action' do
let(:job) do
- create(:environment, name: 'review', project_id: project.id)
- create(:ci_build, :artifacts, :stop_review_app, environment: 'review', pipeline: pipeline, user: api_user, status: job_status)
+ create(:environment, name: 'review/123', project_id: project.id)
+ create(:ci_build, :artifacts, :stop_review_app, environment: 'review/123', pipeline: pipeline, user: api_user, status: job_status)
end
it 'includes environment slug' do
- expect(json_response.dig('environment', 'slug')).to eq('review')
+ expect(json_response.dig('environment', 'slug')).to match('review-123-.*')
end
it 'includes environment tier' do
expect(json_response.dig('environment', 'tier')).to eq('development')
end
+
+ it_behaves_like 'valid allowed_agents request for a job with environment'
end
context 'when passing the token as params' do
@@ -325,7 +386,7 @@ RSpec.describe API::Ci::Jobs do
context 'authorized user' do
it 'returns project jobs' do
expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
+ expect(response).to include_limited_pagination_headers
expect(json_response).to be_an Array
end
@@ -426,6 +487,46 @@ RSpec.describe API::Ci::Jobs do
end
end
+ describe 'GET /projects/:id/jobs rate limited' do
+ let(:query) { {} }
+
+ context 'with the ci_enforce_rate_limits_jobs_api feature flag on' do
+ before do
+ stub_feature_flags(ci_enforce_rate_limits_jobs_api: true)
+
+ allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
+ threshold = Gitlab::ApplicationRateLimiter.rate_limits[:jobs_index][:threshold]
+ allow(strategy).to receive(:increment).and_return(threshold + 1)
+ end
+
+ get api("/projects/#{project.id}/jobs", api_user), params: query
+ end
+
+ it 'enforces rate limits for the endpoint' do
+ expect(response).to have_gitlab_http_status :too_many_requests
+ expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
+ end
+ end
+
+ context 'with the ci_enforce_rate_limits_jobs_api feature flag off' do
+ before do
+ stub_feature_flags(ci_enforce_rate_limits_jobs_api: false)
+
+ allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
+ threshold = Gitlab::ApplicationRateLimiter.rate_limits[:jobs_index][:threshold]
+ allow(strategy).to receive(:increment).and_return(threshold + 1)
+ end
+
+ get api("/projects/#{project.id}/jobs", api_user), params: query
+ end
+
+ it 'makes a successful request' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_limited_pagination_headers
+ end
+ end
+ end
+
describe 'GET /projects/:id/jobs/:job_id' do
before do |example|
unless example.metadata[:skip_before_request]
diff --git a/spec/requests/api/ci/pipeline_schedules_spec.rb b/spec/requests/api/ci/pipeline_schedules_spec.rb
index 30badadde13..2a2c5f65aee 100644
--- a/spec/requests/api/ci/pipeline_schedules_spec.rb
+++ b/spec/requests/api/ci/pipeline_schedules_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::PipelineSchedules do
+RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integration do
let_it_be(:developer) { create(:user) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, public_builds: false) }
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index c9d06f37c8b..6d69da85449 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Pipelines do
+RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:non_member) { create(:user) }
let_it_be(:project2) { create(:project, creator: user) }
diff --git a/spec/requests/api/ci/resource_groups_spec.rb b/spec/requests/api/ci/resource_groups_spec.rb
index 2a67a3e4322..26265aec1dc 100644
--- a/spec/requests/api/ci/resource_groups_spec.rb
+++ b/spec/requests/api/ci/resource_groups_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::ResourceGroups do
+RSpec.describe API::Ci::ResourceGroups, feature_category: :continuous_delivery do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } }
diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
index 9af0541bd2c..1c119079c50 100644
--- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
+RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :runner do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb
index 8c95748aa5f..22817922b1b 100644
--- a/spec/requests/api/ci/runner/jobs_put_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_put_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
+RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :runner do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
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 d69a3f5a980..d15bc9d2dd5 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
+RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :runner do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
@@ -175,6 +175,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
'allow_failure' => true }]
end
+ let(:expected_hooks) do
+ [{ 'name' => 'pre_get_sources_script', 'script' => ["echo 'hello pre_get_sources_script'"] }]
+ end
+
let(:expected_variables) do
[{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true, 'masked' => false },
{ 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true, 'masked' => false },
@@ -230,6 +234,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
'variables' => [{ 'key' => 'MYSQL_ROOT_PASSWORD', 'value' => 'root123.' }], 'pull_policy' => nil }
])
expect(json_response['steps']).to eq(expected_steps)
+ expect(json_response['hooks']).to eq(expected_hooks)
expect(json_response['artifacts']).to eq(expected_artifacts)
expect(json_response['cache']).to match(expected_cache)
expect(json_response['variables']).to include(*expected_variables)
@@ -769,6 +774,19 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
end
end
+
+ context 'when the FF ci_hooks_pre_get_sources_script is disabled' do
+ before do
+ stub_feature_flags(ci_hooks_pre_get_sources_script: false)
+ end
+
+ it 'does not return the pre_get_sources_script' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).not_to have_key('hooks')
+ end
+ end
end
describe 'port support' do
diff --git a/spec/requests/api/ci/runner/jobs_trace_spec.rb b/spec/requests/api/ci/runner/jobs_trace_spec.rb
index d42043a7fe5..de67cec0a27 100644
--- a/spec/requests/api/ci/runner/jobs_trace_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_trace_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Runner, :clean_gitlab_redis_trace_chunks do
+RSpec.describe API::Ci::Runner, :clean_gitlab_redis_trace_chunks, feature_category: :runner do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/ci/runner/runners_delete_spec.rb b/spec/requests/api/ci/runner/runners_delete_spec.rb
index 9d1bae7cce8..65c287a9535 100644
--- a/spec/requests/api/ci/runner/runners_delete_spec.rb
+++ b/spec/requests/api/ci/runner/runners_delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
+RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :runner_fleet do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb
index 47302046865..73f8e87a9fb 100644
--- a/spec/requests/api/ci/runner/runners_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_post_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
+RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :runner_fleet do
describe '/api/v4/runners' do
describe 'POST /api/v4/runners' do
context 'when no token is provided' do
diff --git a/spec/requests/api/ci/runner/runners_reset_spec.rb b/spec/requests/api/ci/runner/runners_reset_spec.rb
index 02b66a89a0a..6ab21138d26 100644
--- a/spec/requests/api/ci/runner/runners_reset_spec.rb
+++ b/spec/requests/api/ci/runner/runners_reset_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
+RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :runner_fleet do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/ci/runner/runners_verify_post_spec.rb b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
index 038e126deaa..22a954cc444 100644
--- a/spec/requests/api/ci/runner/runners_verify_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
+RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :runner do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/ci/runners_reset_registration_token_spec.rb b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
index b8e4370fd46..1110dbf5fbc 100644
--- a/spec/requests/api/ci/runners_reset_registration_token_spec.rb
+++ b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Runners do
+RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
subject { post api("#{prefix}/runners/reset_registration_token", user) }
shared_examples 'bad request' do |result|
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index dd9894f2972..b07dd388390 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Runners do
+RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
diff --git a/spec/requests/api/ci/secure_files_spec.rb b/spec/requests/api/ci/secure_files_spec.rb
index b0bca6e9125..700fd97152a 100644
--- a/spec/requests/api/ci/secure_files_spec.rb
+++ b/spec/requests/api/ci/secure_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::SecureFiles do
+RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do
before do
stub_ci_secure_file_object_storage
stub_feature_flags(ci_secure_files: true)
@@ -31,23 +31,6 @@ RSpec.describe API::Ci::SecureFiles do
end
describe 'GET /projects/:id/secure_files' do
- context 'feature flag' do
- it 'returns a 503 when the feature flag is disabled' do
- stub_feature_flags(ci_secure_files: false)
-
- get api("/projects/#{project.id}/secure_files", maintainer)
-
- expect(response).to have_gitlab_http_status(:service_unavailable)
- end
-
- it 'returns a 200 when the feature flag is enabled' do
- get api("/projects/#{project.id}/secure_files", maintainer)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_a(Array)
- end
- end
-
context 'ci_secure_files_read_only feature flag' do
context 'when the flag is enabled' do
before do
diff --git a/spec/requests/api/ci/triggers_spec.rb b/spec/requests/api/ci/triggers_spec.rb
index f9b7880a4c4..ff54ba61309 100644
--- a/spec/requests/api/ci/triggers_spec.rb
+++ b/spec/requests/api/ci/triggers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Triggers do
+RSpec.describe API::Ci::Triggers, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
diff --git a/spec/requests/api/ci/variables_spec.rb b/spec/requests/api/ci/variables_spec.rb
index cafb841995d..c5d01afb7c4 100644
--- a/spec/requests/api/ci/variables_spec.rb
+++ b/spec/requests/api/ci/variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Variables do
+RSpec.describe API::Ci::Variables, feature_category: :pipeline_authoring do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
diff --git a/spec/requests/api/clusters/agent_tokens_spec.rb b/spec/requests/api/clusters/agent_tokens_spec.rb
index a33bef53b14..b2d996e8002 100644
--- a/spec/requests/api/clusters/agent_tokens_spec.rb
+++ b/spec/requests/api/clusters/agent_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Clusters::AgentTokens do
+RSpec.describe API::Clusters::AgentTokens, feature_category: :kubernetes_management do
let_it_be(:agent) { create(:cluster_agent) }
let_it_be(:agent_token_one) { create(:cluster_agent_token, agent: agent) }
let_it_be(:revoked_agent_token) { create(:cluster_agent_token, :revoked, agent: agent) }
@@ -80,6 +80,27 @@ RSpec.describe API::Clusters::AgentTokens do
end
end
+ it 'returns an agent token that is revoked' do
+ get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens/#{revoked_agent_token.id}", user)
+
+ aggregate_failures "testing response" do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/agent_token')
+ expect(json_response['id']).to eq(revoked_agent_token.id)
+ expect(json_response['name']).to eq(revoked_agent_token.name)
+ expect(json_response['agent_id']).to eq(agent.id)
+ expect(json_response['status']).to eq('revoked')
+ end
+ end
+
+ it 'returns a 404 if agent does not exist' do
+ path = "/projects/#{project.id}/cluster_agents/#{non_existing_record_id}/tokens/#{non_existing_record_id}"
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
it 'returns a 404 error if agent token id is not available' do
get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens/#{non_existing_record_id}", user)
@@ -160,6 +181,21 @@ RSpec.describe API::Clusters::AgentTokens do
expect(agent_token_one.reload).to be_revoked
end
+ it 'returns a success response when revoking an already revoked agent token', :aggregate_failures do
+ delete api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens/#{revoked_agent_token.id}", user)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(revoked_agent_token.reload).to be_revoked
+ end
+
+ it 'returns a 404 error when given agent_id does not exist' do
+ path = "/projects/#{project.id}/cluster_agents/#{non_existing_record_id}/tokens/#{non_existing_record_id}"
+
+ delete api(path, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
it 'returns a 404 error when revoking non existent agent token' do
delete api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens/#{non_existing_record_id}", user)
diff --git a/spec/requests/api/clusters/agents_spec.rb b/spec/requests/api/clusters/agents_spec.rb
index 5e3bdd69529..a09713bd6e7 100644
--- a/spec/requests/api/clusters/agents_spec.rb
+++ b/spec/requests/api/clusters/agents_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Clusters::Agents do
+RSpec.describe API::Clusters::Agents, feature_category: :kubernetes_management do
let_it_be(:agent) { create(:cluster_agent) }
let(:user) { agent.created_by_user }
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index dc5d9620dc4..025d065df7b 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::CommitStatuses do
+RSpec.describe API::CommitStatuses, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:commit) { project.repository.commit }
let_it_be(:guest) { create_user(:guest) }
@@ -167,7 +167,7 @@ RSpec.describe API::CommitStatuses do
let!(:pipeline) { create(:ci_pipeline, project: project, sha: sha, ref: 'ref') }
let(:params) { { state: 'pending' } }
- shared_examples_for 'creates a commit status for the existing pipeline' do
+ shared_examples_for 'creates a commit status for the existing pipeline with an external stage' do
it do
expect do
post api(post_url, developer), params: params
@@ -176,19 +176,73 @@ RSpec.describe API::CommitStatuses do
job = pipeline.statuses.find_by_name(json_response['name'])
expect(response).to have_gitlab_http_status(:created)
+ expect(job.ci_stage.name).to eq('external')
+ expect(job.ci_stage.position).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX)
+ expect(job.ci_stage.pipeline).to eq(pipeline)
expect(job.status).to eq('pending')
expect(job.stage_idx).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX)
end
end
- it_behaves_like 'creates a commit status for the existing pipeline'
+ shared_examples_for 'updates the commit status with an external stage' do
+ before do
+ post api(post_url, developer), params: { state: 'pending' }
+ end
+
+ it 'updates the commit status with the external stage' do
+ post api(post_url, developer), params: { state: 'running' }
+ job = pipeline.statuses.find_by_name(json_response['name'])
+
+ expect(job.ci_stage.name).to eq('external')
+ expect(job.ci_stage.position).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX)
+ expect(job.ci_stage.pipeline).to eq(pipeline)
+ expect(job.status).to eq('running')
+ expect(job.stage_idx).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX)
+ end
+ end
context 'with pipeline for merge request' do
let!(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
let!(:pipeline) { merge_request.all_pipelines.last }
let(:sha) { pipeline.sha }
- it_behaves_like 'creates a commit status for the existing pipeline'
+ it_behaves_like 'creates a commit status for the existing pipeline with an external stage'
+ end
+
+ context 'when an external stage does not exist' do
+ context 'when the commit status does not exist' do
+ it_behaves_like 'creates a commit status for the existing pipeline with an external stage'
+ end
+
+ context 'when the commit status exists' do
+ it_behaves_like 'updates the commit status with an external stage'
+ end
+ end
+
+ context 'when an external stage already exists' do
+ let(:stage) { create(:ci_stage, name: 'external', pipeline: pipeline, position: 1_000_000) }
+
+ context 'when the commit status exists' do
+ it_behaves_like 'updates the commit status with an external stage'
+ end
+
+ context 'when the commit status does not exist' do
+ it_behaves_like 'creates a commit status for the existing pipeline with an external stage'
+ end
+ end
+ end
+
+ context 'when the pipeline does not exist' do
+ it 'creates a commit status and a stage' do
+ expect do
+ post api(post_url, developer), params: { state: 'pending' }
+ end.to change { Ci::Pipeline.count }.by(1)
+ job = Ci::Pipeline.last.statuses.find_by_name(json_response['name'])
+
+ expect(job.ci_stage.name).to eq('external')
+ expect(job.ci_stage.position).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX)
+ expect(job.status).to eq('pending')
+ expect(job.stage_idx).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX)
end
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 8a08d5203fd..5874d764b00 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'mime/types'
-RSpec.describe API::Commits do
+RSpec.describe API::Commits, feature_category: :source_code_management do
include ProjectForksHelper
include SessionHelpers
@@ -492,12 +492,32 @@ RSpec.describe API::Commits do
subject
end
- it_behaves_like 'Snowplow event tracking' do
- let(:namespace) { project.namespace }
- let(:category) { 'ide_edit' }
- let(:action) { 'g_edit_by_web_ide' }
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:namespace) { project.namespace.reload }
+ let(:category) { 'Gitlab::UsageDataCounters::EditorUniqueCounter' }
+ let(:action) { 'ide_edit' }
+ let(:property) { 'g_edit_by_web_ide' }
+ let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_ide_edit' }
+ let(:context) { [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name).to_context] }
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
end
+
+ context 'counts.web_ide_commits Snowplow event tracking' do
+ before do
+ allow(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action)
+ end
+
+ it_behaves_like 'Snowplow event tracking' do
+ let(:action) { :commit }
+ let(:category) { described_class.to_s }
+ let(:namespace) { project.namespace.reload }
+ let(:label) { 'counts.web_ide_commits' }
+ let(:feature_flag_name) { 'route_hll_to_snowplow_phase3' }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: 'counts.web_ide_commits').to_context.to_json]
+ end
+ end
+ end
end
context 'a new file in project repo' do
@@ -2206,7 +2226,7 @@ RSpec.describe API::Commits do
end
describe 'GET /projects/:id/repository/commits/:sha/signature' do
- let!(:project) { create(:project, :repository, :public) }
+ let_it_be(:project) { create(:project, :repository, :public) }
let(:project_id) { project.id }
let(:commit_id) { project.repository.commit.id }
let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/signature" }
@@ -2228,7 +2248,7 @@ RSpec.describe API::Commits do
end
context 'gpg signed commit' do
- let(:commit) { project.repository.commit(GpgHelpers::SIGNED_COMMIT_SHA) }
+ let!(:commit) { project.commit(GpgHelpers::SIGNED_COMMIT_SHA) }
let(:commit_id) { commit.id }
it 'returns correct JSON' do
@@ -2244,8 +2264,8 @@ RSpec.describe API::Commits do
end
context 'x509 signed commit' do
- let(:commit) { project.repository.commit_by(oid: '189a6c924013fc3fe40d6f1ec1dc20214183bc97') }
- let(:commit_id) { commit.id }
+ let(:commit_id) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' }
+ let!(:commit) { project.commit(commit_id) }
it 'returns correct JSON' do
get api(route, current_user)
@@ -2276,5 +2296,59 @@ RSpec.describe API::Commits do
end
end
end
+
+ context 'with ssh signed commit' do
+ let(:commit_id) { '7b5160f9bb23a3d58a0accdbe89da13b96b1ece9' }
+ let!(:commit) { project.commit(commit_id) }
+
+ context 'when key belonging to author does not exist' do
+ it 'returns data without key' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['signature_type']).to eq('SSH')
+ expect(json_response['verification_status']).to eq(commit.signature.verification_status)
+ expect(json_response['key']).to be_nil
+ expect(json_response['commit_source']).to eq('gitaly')
+ end
+ end
+
+ context 'when key belonging to author exists' do
+ let(:user) { create(:user, email: commit.committer_email) }
+ let!(:key) { create(:key, user: user, key: extract_public_key_from_commit(commit), expires_at: 2.days.from_now) }
+
+ def extract_public_key_from_commit(commit)
+ ssh_commit = Gitlab::Ssh::Commit.new(commit)
+ signature_data = ::SSHData::Signature.parse_pem(ssh_commit.signature_text)
+ signature_data.public_key.openssh
+ end
+
+ it 'returns data including key' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['signature_type']).to eq('SSH')
+ expect(json_response['verification_status']).to eq(commit.signature.verification_status)
+ expect(json_response['key']['id']).to eq(key.id)
+ expect(json_response['key']['title']).to eq(key.title)
+ expect(json_response['key']['key']).to eq(key.publishable_key)
+ expect(Time.parse(json_response['key']['created_at'])).to be_like_time(key.created_at)
+ expect(Time.parse(json_response['key']['expires_at'])).to be_like_time(key.expires_at)
+ expect(json_response['commit_source']).to eq('gitaly')
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ssh_commit_signatures: false)
+ end
+
+ it 'returns 404' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/composer_packages_spec.rb b/spec/requests/api/composer_packages_spec.rb
index 53f3ef10743..0c726d46a01 100644
--- a/spec/requests/api/composer_packages_spec.rb
+++ b/spec/requests/api/composer_packages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::ComposerPackages do
+RSpec.describe API::ComposerPackages, feature_category: :package_registry do
include HttpBasicAuthHelpers
let_it_be(:user) { create(:user) }
@@ -14,7 +14,10 @@ RSpec.describe API::ComposerPackages do
let_it_be(:deploy_token_for_group) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token_for_group, group: group) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
+ let(:snowplow_gitlab_standard_context) do
+ { project: project, namespace: project.namespace, user: user, property: 'i_package_composer_user' }
+ end
+
let(:headers) { {} }
using RSpec::Parameterized::TableSyntax
@@ -491,7 +494,6 @@ RSpec.describe API::ComposerPackages do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
@@ -501,6 +503,10 @@ RSpec.describe API::ComposerPackages do
include_context 'Composer user type', params[:user_role], params[:member] do
if params[:expected_status] == :success
+ let(:snowplow_gitlab_standard_context) do
+ { project: project, namespace: project.namespace, property: 'i_package_composer_user' }
+ end
+
it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
else
it_behaves_like 'not a package tracking event'
@@ -509,6 +515,17 @@ RSpec.describe API::ComposerPackages do
end
it_behaves_like 'Composer publish with deploy tokens'
+
+ context 'with access to package registry for everyone' do
+ let(:headers) { {} }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ end
+
+ it_behaves_like 'returning response status', :success
+ end
end
end
diff --git a/spec/requests/api/conan_instance_packages_spec.rb b/spec/requests/api/conan_instance_packages_spec.rb
index b343e0cfc97..0c1d94560b5 100644
--- a/spec/requests/api/conan_instance_packages_spec.rb
+++ b/spec/requests/api/conan_instance_packages_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe API::ConanInstancePackages do
- let(:snowplow_standard_context_params) { { user: user, project: project, namespace: project.namespace } }
+RSpec.describe API::ConanInstancePackages, feature_category: :package_registry do
+ let(:snowplow_gitlab_standard_context) { { user: user, project: project, namespace: project.namespace, property: 'i_package_conan_user' } }
include_context 'conan api setup'
diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb
index 4e6af9942ef..814745f9e29 100644
--- a/spec/requests/api/conan_project_packages_spec.rb
+++ b/spec/requests/api/conan_project_packages_spec.rb
@@ -1,11 +1,21 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::ConanProjectPackages do
+RSpec.describe API::ConanProjectPackages, feature_category: :package_registry do
include_context 'conan api setup'
let(:project_id) { project.id }
- let(:snowplow_standard_context_params) { { user: user, project: project, namespace: project.namespace } }
+
+ shared_examples 'accept get request on private project with access to package registry for everyone' do
+ subject { get api(url) }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ end
+
+ it_behaves_like 'returning response status', :ok
+ end
describe 'GET /api/v4/projects/:id/packages/conan/v1/ping' do
let(:url) { "/projects/#{project.id}/packages/conan/v1/ping" }
@@ -41,43 +51,50 @@ RSpec.describe API::ConanProjectPackages do
include_context 'conan recipe endpoints'
let(:url_prefix) { "#{Settings.gitlab.base_url}/api/v4/projects/#{project_id}" }
+ let(:recipe_path) { package.conan_recipe_path }
+
+ subject { get api(url), headers: headers }
describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
- let(:recipe_path) { package.conan_recipe_path }
let(:url) { "/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}" }
it_behaves_like 'recipe snapshot endpoint'
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference' do
- let(:recipe_path) { package.conan_recipe_path }
let(:url) { "/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}" }
it_behaves_like 'package snapshot endpoint'
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/digest' do
- subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/digest"), headers: headers }
+ let(:url) { "/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/digest" }
it_behaves_like 'recipe download_urls endpoint'
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/download_urls' do
- subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/download_urls"), headers: headers }
+ let(:url) { "/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/download_urls" }
it_behaves_like 'package download_urls endpoint'
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/download_urls' do
- subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/download_urls"), headers: headers }
+ let(:url) { "/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/download_urls" }
it_behaves_like 'recipe download_urls endpoint'
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/digest' do
- subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/digest"), headers: headers }
+ let(:url) { "/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/digest" }
it_behaves_like 'package download_urls endpoint'
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'POST /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/upload_urls' do
@@ -102,24 +119,22 @@ RSpec.describe API::ConanProjectPackages do
context 'file download endpoints', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/326194' do
include_context 'conan file download endpoints'
+ subject { get api(url), headers: headers }
+
describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do
- subject do
- get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"),
- headers: headers
- end
+ let(:url) { "/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}" }
it_behaves_like 'recipe file download endpoint'
it_behaves_like 'project not found by project id'
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
- subject do
- get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"),
- headers: headers
- end
+ let(:url) { "/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}" }
it_behaves_like 'package file download endpoint'
it_behaves_like 'project not found by project id'
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
end
diff --git a/spec/requests/api/container_registry_event_spec.rb b/spec/requests/api/container_registry_event_spec.rb
index 767e6e0b2ff..32c4b0e9598 100644
--- a/spec/requests/api/container_registry_event_spec.rb
+++ b/spec/requests/api/container_registry_event_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ContainerRegistryEvent do
+RSpec.describe API::ContainerRegistryEvent, feature_category: :container_registry do
let(:secret_token) { 'secret_token' }
let(:events) { [{ action: 'push' }] }
let(:registry_headers) { { 'Content-Type' => ::API::ContainerRegistryEvent::DOCKER_DISTRIBUTION_EVENTS_V1_JSON } }
diff --git a/spec/requests/api/container_repositories_spec.rb b/spec/requests/api/container_repositories_spec.rb
index 90f0243dbfc..4c1e52df4fc 100644
--- a/spec/requests/api/container_repositories_spec.rb
+++ b/spec/requests/api/container_repositories_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ContainerRepositories do
+RSpec.describe API::ContainerRepositories, feature_category: :container_registry do
include_context 'container registry client stubs'
let_it_be(:project) { create(:project, :private) }
@@ -75,6 +75,13 @@ RSpec.describe API::ContainerRepositories do
expect(json_response['id']).to eq(repository.id)
expect(response.body).to include('tags')
+ expect(json_response['tags']).to eq(repository.tags.map do |tag|
+ {
+ "location" => tag.location,
+ "name" => tag.name,
+ "path" => tag.path
+ }
+ end)
end
context 'with a network error' do
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
index 9dbb75becf8..f4d5ef3fe90 100644
--- a/spec/requests/api/debian_group_packages_spec.rb
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::DebianGroupPackages do
+RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
include HttpBasicAuthHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
index 6bef669cb3a..c27e165b39b 100644
--- a/spec/requests/api/debian_project_packages_spec.rb
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::DebianProjectPackages do
+RSpec.describe API::DebianProjectPackages, feature_category: :package_registry do
include HttpBasicAuthHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/dependency_proxy_spec.rb b/spec/requests/api/dependency_proxy_spec.rb
index 7af4ed08cb8..ef94cdbbe2b 100644
--- a/spec/requests/api/dependency_proxy_spec.rb
+++ b/spec/requests/api/dependency_proxy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::DependencyProxy, api: true do
+RSpec.describe API::DependencyProxy, api: true, feature_category: :dependency_proxy do
let_it_be(:user) { create(:user) }
let_it_be(:blob) { create(:dependency_proxy_blob) }
let_it_be(:group, reload: true) { blob.group }
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 1daa7c38e04..15880d920c5 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::DeployKeys do
+RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
let_it_be(:user) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:admin) { create(:admin) }
diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb
index e0296248a03..4efe49e843f 100644
--- a/spec/requests/api/deploy_tokens_spec.rb
+++ b/spec/requests/api/deploy_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::DeployTokens do
+RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do
let_it_be(:user) { create(:user) }
let_it_be(:creator) { create(:user) }
let_it_be(:project) { create(:project, creator_id: creator.id) }
@@ -346,7 +346,7 @@ RSpec.describe API::DeployTokens do
context 'deploy token creation' do
shared_examples 'creating a deploy token' do |entity, unauthenticated_response, authorized_role|
- let(:expires_time) { 1.year.from_now }
+ let(:expires_time) { 1.year.from_now.to_datetime }
let(:params) do
{
name: 'Foo',
@@ -414,6 +414,14 @@ RSpec.describe API::DeployTokens do
it { is_expected.to have_gitlab_http_status(:bad_request) }
end
+
+ context 'with an invalid expires_at date' do
+ before do
+ params[:expires_at] = 'foo'
+ end
+
+ it { is_expected.to have_gitlab_http_status(:bad_request) }
+ end
end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 8124080abea..efe76c9cfda 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Deployments do
+RSpec.describe API::Deployments, feature_category: :continuous_delivery do
let_it_be(:user) { create(:user) }
let_it_be(:non_member) { create(:user) }
@@ -16,8 +16,8 @@ RSpec.describe API::Deployments do
let_it_be(:staging) { create(:environment, :staging, project: project) }
let_it_be(:build) { create(:ci_build, :success, project: project) }
let_it_be(:deployment_1) { create(:deployment, :success, project: project, environment: production, deployable: build, ref: 'master', created_at: Time.now, updated_at: Time.now) }
- let_it_be(:deployment_2) { create(:deployment, :success, project: project, environment: staging, deployable: build, ref: 'master', created_at: 1.day.ago, updated_at: 2.hours.ago) }
- let_it_be(:deployment_3) { create(:deployment, :success, project: project, environment: staging, deployable: build, ref: 'master', created_at: 2.days.ago, updated_at: 1.hour.ago) }
+ let_it_be(:deployment_2) { create(:deployment, :success, project: project, environment: staging, deployable: build, ref: 'master', created_at: 1.day.ago, finished_at: 2.hours.ago, updated_at: 2.hours.ago) }
+ let_it_be(:deployment_3) { create(:deployment, :success, project: project, environment: staging, deployable: build, ref: 'master', created_at: 2.days.ago, finished_at: 1.hour.ago, updated_at: 1.hour.ago) }
def perform_request(params = {})
get api("/projects/#{project.id}/deployments", user), params: params
@@ -47,7 +47,7 @@ RSpec.describe API::Deployments do
end
context 'when forbidden order_by is specified' do
- it 'returns projects deployments with last update in specified datetime range' do
+ it 'returns an error' do
perform_request({ updated_before: 30.minutes.ago, updated_after: 90.minutes.ago, order_by: :id })
expect(response).to have_gitlab_http_status(:bad_request)
@@ -56,6 +56,44 @@ RSpec.describe API::Deployments do
end
end
+ context 'with finished after and before filters specified' do
+ context 'for successful deployments' do
+ it 'returns projects deployments finished before the specified datetime range' do
+ perform_request({ status: :success, finished_before: 90.minutes.ago, order_by: :finished_at, environment: 'staging' })
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.first['id']).to eq(deployment_2.id)
+ end
+
+ it 'returns projects deployments finished after the specified datetime range' do
+ perform_request({ status: :success, finished_after: 90.minutes.ago, order_by: :finished_at, environment: 'staging' })
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.first['id']).to eq(deployment_3.id)
+ end
+ end
+
+ context 'for unsuccessful deployments' do
+ it 'returns an error' do
+ perform_request({ status: :failed, finished_before: 30.minutes.ago, order_by: :finished_at })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to include('`finished_at` filter must be combined with `success` status filter.')
+ end
+ end
+
+ context 'when a forbidden order_by is specified' do
+ it 'returns an error' do
+ perform_request({ status: :success, finished_before: 30.minutes.ago, order_by: :id })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to include('`finished_at` filter requires `finished_at` sort.')
+ end
+ end
+ end
+
context 'with the environment filter specifed' do
it 'returns deployments for the environment' do
perform_request({ environment: production.name })
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index 258bd26c05a..38016375b8f 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Discussions do
+RSpec.describe API::Discussions, feature_category: :team_planning do
let(:user) { create(:user) }
let!(:project) { create(:project, :public, :repository, namespace: user.namespace) }
let(:private_user) { create(:user) }
@@ -29,6 +29,73 @@ RSpec.describe API::Discussions do
end
end
+ context 'when noteable is a WorkItem' do
+ let!(:work_item) { create(:work_item, :issue, project: project, author: user) }
+ let!(:work_item_note) { create(:discussion_note_on_issue, noteable: work_item, project: project, author: user) }
+
+ let(:parent) { project }
+ let(:noteable) { work_item }
+ let(:note) { work_item_note }
+ let(:url) { "/projects/#{parent.id}/issues/#{noteable[:iid]}/discussions" }
+
+ it_behaves_like 'discussions API', 'projects', 'issues', 'iid', can_reply_to_individual_notes: true
+
+ context 'with work item without notes widget' do
+ before do
+ stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
+ stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+ end
+
+ context 'when fetching discussions' do
+ it "returns 404" do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when single fetching discussion by discussion_id' do
+ it "returns 404" do
+ get api("#{url}/#{work_item_note.discussion_id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when trying to create a new discussion' do
+ it "returns 404" do
+ post api(url, user), params: { body: 'hi!' }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when trying to create a new comment on a discussion' do
+ it 'returns 404' do
+ post api("#{url}/#{note.discussion_id}/notes", user), params: { body: 'Hello!' }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when trying to update a new comment on a discussion' do
+ it 'returns 404' do
+ put api("#{url}/notes/#{note.id}", user), params: { body: 'Update Hello!' }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when deleting a note' do
+ it 'returns 404' do
+ delete api("#{url}/#{note.discussion_id}/notes/#{note.id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
context 'when noteable is a Snippet' do
let!(:snippet) { create(:project_snippet, project: project, author: user) }
let!(:snippet_note) { create(:discussion_note_on_project_snippet, noteable: snippet, project: project, author: user) }
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index 14da9a600cd..5116f074894 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'doorkeeper access' do
+RSpec.describe 'doorkeeper access', feature_category: :authentication_and_authorization do
let!(:user) { create(:user) }
let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" }
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index a35c1630caa..d06e70a1a02 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Environments do
+RSpec.describe API::Environments, feature_category: :continuous_delivery do
let_it_be(:user) { create(:user) }
let_it_be(:non_member) { create(:user) }
let_it_be(:project) { create(:project, :private, :repository, namespace: user.namespace) }
@@ -321,8 +321,8 @@ RSpec.describe API::Environments do
expect(json_response["scheduled_entries"].size).to eq(1)
expect(json_response["scheduled_entries"].first["id"]).to eq(old_stopped_review_env.id)
expect(json_response["unprocessable_entries"].size).to eq(0)
- expect(json_response["scheduled_entries"]).to match_schema('public_api/v4/environments')
- expect(json_response["unprocessable_entries"]).to match_schema('public_api/v4/environments')
+ expect(json_response["scheduled_entries"]).to match_schema('public_api/v4/basic_environments')
+ expect(json_response["unprocessable_entries"]).to match_schema('public_api/v4/basic_environments')
expect(old_stopped_review_env.reload.auto_delete_at).to eq(1.week.from_now)
expect(new_stopped_review_env.reload.auto_delete_at).to be_nil
diff --git a/spec/requests/api/error_tracking/client_keys_spec.rb b/spec/requests/api/error_tracking/client_keys_spec.rb
index ba4d713dff2..cb840e1cffa 100644
--- a/spec/requests/api/error_tracking/client_keys_spec.rb
+++ b/spec/requests/api/error_tracking/client_keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ErrorTracking::ClientKeys do
+RSpec.describe API::ErrorTracking::ClientKeys, feature_category: :error_tracking do
let_it_be(:guest) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:setting) { create(:project_error_tracking_setting) }
diff --git a/spec/requests/api/error_tracking/collector_spec.rb b/spec/requests/api/error_tracking/collector_spec.rb
index dfca994d1c3..6a3e71bc859 100644
--- a/spec/requests/api/error_tracking/collector_spec.rb
+++ b/spec/requests/api/error_tracking/collector_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ErrorTracking::Collector do
+RSpec.describe API::ErrorTracking::Collector, feature_category: :error_tracking do
let_it_be(:project) { create(:project, :private) }
let_it_be(:setting) { create(:project_error_tracking_setting, :integrated, project: project) }
let_it_be(:client_key) { create(:error_tracking_client_key, project: project) }
diff --git a/spec/requests/api/error_tracking/project_settings_spec.rb b/spec/requests/api/error_tracking/project_settings_spec.rb
index c0c0680ef31..5906cdf105a 100644
--- a/spec/requests/api/error_tracking/project_settings_spec.rb
+++ b/spec/requests/api/error_tracking/project_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ErrorTracking::ProjectSettings do
+RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tracking do
let_it_be(:user) { create(:user) }
let(:setting) { create(:project_error_tracking_setting) }
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index d6c3999f22f..5c061a37ff3 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Events do
+RSpec.describe API::Events, feature_category: :users do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/requests/api/feature_flags_spec.rb b/spec/requests/api/feature_flags_spec.rb
index bf7eec167f5..69e3633de57 100644
--- a/spec/requests/api/feature_flags_spec.rb
+++ b/spec/requests/api/feature_flags_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::FeatureFlags do
+RSpec.describe API::FeatureFlags, feature_category: :feature_flags do
include FeatureFlagHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/feature_flags_user_lists_spec.rb b/spec/requests/api/feature_flags_user_lists_spec.rb
index bfc57042ff4..443cbbea147 100644
--- a/spec/requests/api/feature_flags_user_lists_spec.rb
+++ b/spec/requests/api/feature_flags_user_lists_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::FeatureFlagsUserLists do
+RSpec.describe API::FeatureFlagsUserLists, feature_category: :feature_flags do
let_it_be(:project, refind: true) { create(:project) }
let_it_be(:client, refind: true) { create(:operations_feature_flags_client, project: project) }
let_it_be(:developer) { create(:user) }
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index 85dafef569d..9f1af746080 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -2,8 +2,9 @@
require 'spec_helper'
-RSpec.describe API::Features, stub_feature_flags: false do
- let_it_be(:user) { create(:user) }
+RSpec.describe API::Features, stub_feature_flags: false, feature_category: :feature_flags do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:opted_out) { create(:user) }
let_it_be(:admin) { create(:admin) }
# Find any `development` feature flag name
@@ -35,7 +36,10 @@ RSpec.describe API::Features, stub_feature_flags: false do
{
'name' => 'feature_1',
'state' => 'on',
- 'gates' => [{ 'key' => 'boolean', 'value' => true }],
+ 'gates' => [
+ { 'key' => 'boolean', 'value' => true },
+ { 'key' => 'actors', 'value' => ["#{opted_out.flipper_id}:opt_out"] }
+ ],
'definition' => nil
},
{
@@ -64,6 +68,7 @@ RSpec.describe API::Features, stub_feature_flags: false do
before do
Feature.enable('feature_1')
+ Feature.opt_out('feature_1', opted_out)
Feature.disable('feature_2')
Feature.enable('feature_3', Feature.group(:perf_team))
Feature.enable(known_feature_flag.name)
@@ -654,12 +659,53 @@ RSpec.describe API::Features, stub_feature_flags: false do
it_behaves_like 'sets the feature flag status'
+ it 'opts given actors out' do
+ Feature.enable(feature_name)
+ expect(Feature.enabled?(feature_name, user)).to be_truthy
+
+ post api("/features/#{feature_name}", admin), params: { value: 'opt_out', user: user.username }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to include(
+ 'name' => feature_name,
+ 'state' => 'on',
+ 'gates' => [
+ { 'key' => 'boolean', 'value' => true },
+ { 'key' => 'actors', 'value' => ["#{user.flipper_id}:opt_out"] }
+ ]
+ )
+ end
+
+ context 'when the actor has opted-out' do
+ before do
+ Feature.enable(feature_name)
+ Feature.opt_out(feature_name, user)
+ end
+
+ it 'refuses to enable the feature' do
+ post api("/features/#{feature_name}", admin), params: { value: 'true', user: user.username }
+
+ expect(Feature).not_to be_enabled(feature_name, user)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
context 'when feature flag set_feature_flag_service is disabled' do
before do
stub_feature_flags(set_feature_flag_service: false)
end
it_behaves_like 'sets the feature flag status'
+
+ it 'rejects opt_out requests' do
+ Feature.enable(feature_name)
+ expect(Feature).to be_enabled(feature_name, user)
+
+ post api("/features/#{feature_name}", admin), params: { value: 'opt_out', user: user.username }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index d4d3aace204..9cee3c06bb1 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Files do
+RSpec.describe API::Files, feature_category: :source_code_management do
include RepoHelpers
let_it_be(:group) { create(:group, :public) }
@@ -11,8 +11,12 @@ RSpec.describe API::Files do
let_it_be(:inherited_reporter) { create(:user) }
let_it_be(:inherited_developer) { create(:user) }
- let!(:project) { create(:project, :repository, namespace: user.namespace) }
- let(:guest) { create(:user) { |u| project.add_guest(u) } }
+ let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) }
+ let_it_be_with_reload(:public_project) { create(:project, :public, :repository) }
+ let_it_be_with_reload(:private_project) { create(:project, :private, :repository, group: group) }
+ let_it_be_with_reload(:public_project_private_repo) { create(:project, :public, :repository, :repository_private, group: group) }
+
+ let_it_be(:guest) { create(:user) { |u| project.add_guest(u) } }
let(:file_path) { 'files%2Fruby%2Fpopen%2Erb' }
let(:file_name) { 'popen.rb' }
let(:last_commit_id) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' }
@@ -183,8 +187,9 @@ RSpec.describe API::Files do
context 'when unauthenticated' do
context 'and project is public' do
+ let(:project) { public_project }
+
it_behaves_like 'repository files' do
- let(:project) { create(:project, :public, :repository) }
let(:current_user) { nil }
end
end
@@ -361,7 +366,7 @@ RSpec.describe API::Files do
context 'when unauthenticated' do
context 'and project is public' do
it_behaves_like 'repository files' do
- let(:project) { create(:project, :public, :repository) }
+ let(:project) { public_project }
let(:current_user) { nil }
let(:api_user) { nil }
end
@@ -406,7 +411,7 @@ RSpec.describe API::Files do
context 'when authenticated' do
context 'and user is an inherited member from the group' do
context 'when project is public with private repository' do
- let_it_be(:project) { create(:project, :public, :repository, :repository_private, group: group) }
+ let(:project) { public_project_private_repo }
context 'and user is a guest' do
it_behaves_like 'returns non-executable file attributes as json' do
@@ -428,7 +433,7 @@ RSpec.describe API::Files do
end
context 'when project is private' do
- let_it_be(:project) { create(:project, :private, :repository, group: group) }
+ let(:project) { private_project }
context 'and user is a guest' do
it_behaves_like '403 response' do
@@ -655,7 +660,7 @@ RSpec.describe API::Files do
context 'when unauthenticated' do
context 'and project is public' do
it_behaves_like 'repository blame files' do
- let(:project) { create(:project, :public, :repository) }
+ let(:project) { public_project }
let(:current_user) { nil }
end
end
@@ -774,12 +779,69 @@ RSpec.describe API::Files do
let(:request) { get api(route(file_path), current_user), params: params }
end
end
+
+ context 'when lfs parameter is true and the project has lfs enabled' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
+ end
+
+ let(:request) { get api(route(file_path) + '/raw', current_user), params: params.merge(lfs: true) }
+ let(:file_path) { 'files%2Flfs%2Flfs_object.iso' }
+
+ it_behaves_like '404 response'
+
+ context 'and the file has an lfs object' do
+ let_it_be(:lfs_object) { create(:lfs_object, :with_file, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897') }
+
+ it_behaves_like '404 response'
+
+ context 'and the project has access to the lfs object' do
+ before do
+ project.lfs_objects << lfs_object
+ end
+
+ context 'and lfs uses local file storage' do
+ before do
+ Grape::Endpoint.before_each do |endpoint|
+ allow(endpoint).to receive(:sendfile).with(lfs_object.file.path)
+ end
+ end
+
+ after do
+ Grape::Endpoint.before_each nil
+ end
+
+ it 'responds with the lfs object file' do
+ request
+ expect(response.headers["Content-Disposition"]).to eq(
+ "attachment; filename=\"#{lfs_object.file.filename}\"; filename*=UTF-8''#{lfs_object.file.filename}"
+ )
+ end
+ end
+
+ context 'and lfs uses remote object storage' do
+ before do
+ stub_lfs_object_storage
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
+
+ it 'redirects to the lfs object file' do
+ request
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response.location).to include(lfs_object.reload.file.path)
+ end
+ end
+ end
+ end
+ end
end
context 'when unauthenticated' do
context 'and project is public' do
it_behaves_like 'repository raw files' do
- let(:project) { create(:project, :public, :repository) }
+ let(:project) { public_project }
let(:current_user) { nil }
end
end
@@ -821,7 +883,7 @@ RSpec.describe API::Files do
end
describe 'POST /projects/:id/repository/files/:file_path' do
- let!(:file_path) { 'new_subfolder%2Fnewfile%2Erb' }
+ let(:file_path) { FFaker::Guid.guid }
let(:params) do
{
@@ -939,14 +1001,13 @@ RSpec.describe API::Files do
it_behaves_like 'creates a new file in the project repo' do
let(:current_user) { user }
- let(:file_path) { 'newfile%2Erb' }
+ let(:file_path) { FFaker::Guid.guid }
end
end
context 'when specifying an author' do
it 'creates a new file with the specified author' do
params.merge!(author_email: author_email, author_name: author_name)
-
post api(route('new_file_with_author%2Etxt'), user), params: params
expect(response).to have_gitlab_http_status(:created)
@@ -963,7 +1024,7 @@ RSpec.describe API::Files do
context 'when authenticated' do
context 'and user is an inherited member from the group' do
context 'when project is public with private repository' do
- let_it_be(:project) { create(:project, :public, :repository, :repository_private, group: group) }
+ let(:project) { public_project_private_repo }
context 'and user is a guest' do
it_behaves_like '403 response' do
@@ -985,7 +1046,7 @@ RSpec.describe API::Files do
end
context 'when project is private' do
- let_it_be(:project) { create(:project, :private, :repository, group: group) }
+ let(:project) { private_project }
context 'and user is a guest' do
it_behaves_like '403 response' do
@@ -1161,64 +1222,76 @@ RSpec.describe API::Files do
}
end
- it 'returns 400 when file path is invalid' do
- delete api(route(invalid_file_path), user), params: params
+ describe 'when files are deleted' do
+ let(:file_path) { FFaker::Guid.guid }
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq(invalid_file_message)
- end
+ before do
+ create_file_in_repo(project, 'master', 'master', file_path, 'Test file')
+ end
- it_behaves_like 'when path is absolute' do
- subject { delete api(route(absolute_path), user), params: params }
- end
+ it 'deletes existing file in project repo' do
+ delete api(route(file_path), user), params: params
- it 'deletes existing file in project repo' do
- delete api(route(file_path), user), params: params
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
- expect(response).to have_gitlab_http_status(:no_content)
- end
+ context 'when specifying an author' do
+ before do
+ params.merge!(author_email: author_email, author_name: author_name)
+ end
- context 'when no params given' do
- it 'returns a 400 bad request' do
- delete api(route(file_path), user)
+ it 'removes a file with the specified author' do
+ delete api(route(file_path), user), params: params
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
end
end
- context 'when the commit message is empty' do
- before do
- params[:commit_message] = ''
+ describe 'when files are not deleted' do
+ it_behaves_like 'when path is absolute' do
+ subject { delete api(route(absolute_path), user), params: params }
end
- it 'returns a 400 bad request' do
- delete api(route(file_path), user), params: params
+ it 'returns 400 when file path is invalid' do
+ delete api(route(invalid_file_path), user), params: params
expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq(invalid_file_message)
end
- end
- context 'when fails to delete file' do
- before do
- allow_next_instance_of(Repository) do |instance|
- allow(instance).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file')
+ context 'when no params given' do
+ it 'returns a 400 bad request' do
+ delete api(route(file_path), user)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
end
end
- it 'returns a 400 bad request' do
- delete api(route(file_path), user), params: params
+ context 'when the commit message is empty' do
+ before do
+ params[:commit_message] = ''
+ end
- expect(response).to have_gitlab_http_status(:bad_request)
+ it 'returns a 400 bad request' do
+ delete api(route(file_path), user), params: params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- end
- context 'when specifying an author' do
- it 'removes a file with the specified author' do
- params.merge!(author_email: author_email, author_name: author_name)
+ context 'when fails to delete file' do
+ before do
+ allow_next_instance_of(Repository) do |instance|
+ allow(instance).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file')
+ end
+ end
- delete api(route(file_path), user), params: params
+ it 'returns a 400 bad request' do
+ delete api(route(file_path), user), params: params
- expect(response).to have_gitlab_http_status(:no_content)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
end
end
diff --git a/spec/requests/api/freeze_periods_spec.rb b/spec/requests/api/freeze_periods_spec.rb
index 3da992301d5..170871706dc 100644
--- a/spec/requests/api/freeze_periods_spec.rb
+++ b/spec/requests/api/freeze_periods_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::FreezePeriods do
+RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
let_it_be(:project) { create(:project, :repository, :private) }
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
index 0478e123086..6b3f378a4bc 100644
--- a/spec/requests/api/generic_packages_spec.rb
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GenericPackages do
+RSpec.describe API::GenericPackages, feature_category: :package_registry do
include HttpBasicAuthHelpers
using RSpec::Parameterized::TableSyntax
@@ -19,7 +19,7 @@ RSpec.describe API::GenericPackages do
let(:user) { personal_access_token.user }
let(:ci_build) { create(:ci_build, :running, user: user, project: project) }
- let(:snowplow_standard_context_params) { { user: user, project: project, namespace: project.namespace } }
+ let(:snowplow_gitlab_standard_context) { { user: user, project: project, namespace: project.namespace, property: 'i_package_generic_user' } }
def auth_header
return {} if user_role == :anonymous
@@ -408,8 +408,6 @@ RSpec.describe API::GenericPackages do
end
context 'event tracking' do
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
-
subject { upload_file(params, workhorse_headers.merge(personal_access_token_header)) }
it_behaves_like 'a package tracking event', described_class.name, 'push_package'
@@ -645,8 +643,6 @@ RSpec.describe API::GenericPackages do
end
context 'event tracking' do
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
-
before do
project.add_developer(user)
end
diff --git a/spec/requests/api/geo_spec.rb b/spec/requests/api/geo_spec.rb
index 4e77fa9405c..3dec91fd2fa 100644
--- a/spec/requests/api/geo_spec.rb
+++ b/spec/requests/api/geo_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Geo do
+RSpec.describe API::Geo, feature_category: :geo_replication do
include WorkhorseHelpers
describe 'GET /geo/proxy' do
diff --git a/spec/requests/api/go_proxy_spec.rb b/spec/requests/api/go_proxy_spec.rb
index 5498ed6df13..17189087ade 100644
--- a/spec/requests/api/go_proxy_spec.rb
+++ b/spec/requests/api/go_proxy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GoProxy do
+RSpec.describe API::GoProxy, feature_category: :package_registry do
include PackagesManagerApiSpecHelpers
include HttpBasicAuthHelpers
diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
index 9bed720c815..2775c3d4c5a 100644
--- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'get board lists' do
+RSpec.describe 'get board lists', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/boards/board_list_query_spec.rb b/spec/requests/api/graphql/boards/board_list_query_spec.rb
index f01f7e87f10..b5ed0fe35d5 100644
--- a/spec/requests/api/graphql/boards/board_list_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_list_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Querying a Board list' do
+RSpec.describe 'Querying a Board list', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
index ad7df5c9344..2f23e93e2c6 100644
--- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'get board lists' do
+RSpec.describe 'get board lists', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/boards/boards_query_spec.rb b/spec/requests/api/graphql/boards/boards_query_spec.rb
index 50004e5a8a1..1407034fa5f 100644
--- a/spec/requests/api/graphql/boards/boards_query_spec.rb
+++ b/spec/requests/api/graphql/boards/boards_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'get list of boards' do
+RSpec.describe 'get list of boards', feature_category: :team_planning do
include GraphqlHelpers
include_context 'group and project boards query context'
diff --git a/spec/requests/api/graphql/ci/application_setting_spec.rb b/spec/requests/api/graphql/ci/application_setting_spec.rb
index 156ee550f16..42ab1786fee 100644
--- a/spec/requests/api/graphql/ci/application_setting_spec.rb
+++ b/spec/requests/api/graphql/ci/application_setting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting Application Settings' do
+RSpec.describe 'getting Application Settings', feature_category: :continuous_integration do
include GraphqlHelpers
let(:fields) do
diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
index 2dc7b9764fe..0437a30eccd 100644
--- a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Getting Ci Cd Setting' do
+RSpec.describe 'Getting Ci Cd Setting', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be_with_reload(:project) { create(:project, :repository) }
diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb
index 784019ee926..8154f132430 100644
--- a/spec/requests/api/graphql/ci/config_spec.rb
+++ b/spec/requests/api/graphql/ci/config_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.ciConfig' do
+RSpec.describe 'Query.ciConfig', feature_category: :continuous_integration do
include GraphqlHelpers
include StubRequests
diff --git a/spec/requests/api/graphql/ci/config_variables_spec.rb b/spec/requests/api/graphql/ci/config_variables_spec.rb
index 17133d7ea66..e6d73701b8f 100644
--- a/spec/requests/api/graphql/ci/config_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/config_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)' do
+RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)', feature_category: :pipeline_authoring do
include GraphqlHelpers
include ReactiveCachingHelpers
diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb
index 7baf26c7648..51cbb4719f7 100644
--- a/spec/requests/api/graphql/ci/group_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/group_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.group(fullPath).ciVariables' do
+RSpec.describe 'Query.group(fullPath).ciVariables', feature_category: :pipeline_authoring do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/ci/groups_spec.rb b/spec/requests/api/graphql/ci/groups_spec.rb
index d1a4395d2c9..d1588833d8f 100644
--- a/spec/requests/api/graphql/ci/groups_spec.rb
+++ b/spec/requests/api/graphql/ci/groups_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Query.project.pipeline.stages.groups' do
+RSpec.describe 'Query.project.pipeline.stages.groups', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb
index cd6b2de98a1..e0397e17923 100644
--- a/spec/requests/api/graphql/ci/instance_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.ciVariables' do
+RSpec.describe 'Query.ciVariables', feature_category: :pipeline_authoring do
include GraphqlHelpers
let(:query) do
diff --git a/spec/requests/api/graphql/ci/job_artifacts_spec.rb b/spec/requests/api/graphql/ci/job_artifacts_spec.rb
index df6e398fbe5..5fcb363d479 100644
--- a/spec/requests/api/graphql/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/graphql/ci/job_artifacts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).pipelines.jobs.artifacts' do
+RSpec.describe 'Query.project(fullPath).pipelines.jobs.artifacts', feature_category: :build do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb
index 3721155c71b..8121c5e5c85 100644
--- a/spec/requests/api/graphql/ci/job_spec.rb
+++ b/spec/requests/api/graphql/ci/job_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
+RSpec.describe 'Query.project(fullPath).pipelines.job(id)', feature_category: :continuous_integration do
include GraphqlHelpers
around do |example|
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index a161c5c98ed..7a1dc614dcf 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Query.project.pipeline' do
+RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
@@ -367,4 +367,65 @@ RSpec.describe 'Query.project.pipeline' do
expect_graphql_errors_to_include [/"jobs" field can be requested only for 1 Project\(s\) at a time./]
end
end
+
+ context 'when batched querying jobs for multiple projects' do
+ let(:batched) do
+ [
+ { query: query_1 },
+ { query: query_2 }
+ ]
+ end
+
+ let(:query_1) do
+ %(
+ query Page1 {
+ projects {
+ nodes {
+ jobs {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ let(:query_2) do
+ %(
+ query Page2 {
+ projects {
+ nodes {
+ jobs {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ create_list(:project, 2).each do |project|
+ project.add_developer(user)
+ create(:ci_build, project: project)
+ end
+ end
+
+ it 'limits the specific field evaluation per query' do
+ get_multiplex(batched, current_user: user)
+
+ resp = json_response
+
+ expect(resp.first.dig('data', 'projects', 'nodes').first.dig('jobs', 'nodes').first['name']).to eq('test')
+ expect(resp.first['errors'].first['message'])
+ .to match(/"jobs" field can be requested only for 1 Project\(s\) at a time./)
+ expect(resp.second.dig('data', 'projects', 'nodes').first.dig('jobs', 'nodes').first['name']).to eq('test')
+ expect(resp.second['errors'].first['message'])
+ .to match(/"jobs" field can be requested only for 1 Project\(s\) at a time./)
+ 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 a15bac2b8bd..921c69e535d 100644
--- a/spec/requests/api/graphql/ci/manual_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables' do
+RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables', feature_category: :pipeline_authoring do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/ci/pipeline_schedules_spec.rb b/spec/requests/api/graphql/ci/pipeline_schedules_spec.rb
index 8b8ba09a95c..76adce6ff1b 100644
--- a/spec/requests/api/graphql/ci/pipeline_schedules_spec.rb
+++ b/spec/requests/api/graphql/ci/pipeline_schedules_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe 'Query.project.pipelineSchedules' do
+RSpec.describe 'Query.project.pipelineSchedules', feature_category: :continuous_integration do
include GraphqlHelpers
- let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository, :public, creator: user, namespace: user.namespace) }
let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
let(:pipeline_schedule_graphql_data) { graphql_data_at(:project, :pipeline_schedules, :nodes, 0) }
@@ -29,6 +29,8 @@ RSpec.describe 'Query.project.pipelineSchedules' do
forTag
cron
cronTimezone
+ editPath
+ variables { nodes { #{all_graphql_fields_for('PipelineScheduleVariable')} } }
}
QUERY
end
@@ -61,6 +63,58 @@ RSpec.describe 'Query.project.pipelineSchedules' do
expect(pipeline_schedule_graphql_data['refPath']).to eq("/#{project.full_path}/-/commits/#{ref_for_display}")
expect(pipeline_schedule_graphql_data['forTag']).to be(false)
end
+
+ it 'returns the edit_path for a pipeline schedule' do
+ edit_path = pipeline_schedule_graphql_data['editPath']
+
+ expect(edit_path).to eq("/#{project.full_path}/-/pipeline_schedules/#{pipeline_schedule.id}/edit")
+ end
+ end
+
+ describe 'variables' do
+ let!(:env_vars) { create_list(:ci_pipeline_schedule_variable, 5, pipeline_schedule: pipeline_schedule) }
+
+ it 'returns all variables' do
+ post_graphql(query, current_user: user)
+
+ variables = pipeline_schedule_graphql_data['variables']['nodes']
+ expected = env_vars.map do |var|
+ a_graphql_entity_for(var, :key, :value, variable_type: var.variable_type.upcase)
+ end
+
+ expect(variables).to match_array(expected)
+ end
+
+ it 'is N+1 safe on the variables level' do
+ baseline = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: user) }
+
+ create_list(:ci_pipeline_schedule_variable, 2, pipeline_schedule: pipeline_schedule)
+
+ expect { post_graphql(query, current_user: user) }.not_to exceed_query_limit(baseline)
+ end
+
+ it 'is N+1 safe on the schedules level' do
+ baseline = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: user) }
+
+ pipeline_schedule_2 = create(:ci_pipeline_schedule, project: project, owner: user)
+ create_list(:ci_pipeline_schedule_variable, 2, pipeline_schedule: pipeline_schedule_2)
+
+ expect { post_graphql(query, current_user: user) }.not_to exceed_query_limit(baseline)
+ end
+ end
+
+ describe 'permissions' do
+ let_it_be(:another_user) { create(:user) }
+
+ before do
+ post_graphql(query, current_user: another_user)
+ end
+
+ it 'does not return the edit_path for a pipeline schedule for a user that does not have permissions' do
+ edit_path = pipeline_schedule_graphql_data['editPath']
+
+ expect(edit_path).to be nil
+ end
end
it 'avoids N+1 queries' do
diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb
index 948704e8770..9fe71533b5e 100644
--- a/spec/requests/api/graphql/ci/pipelines_spec.rb
+++ b/spec/requests/api/graphql/ci/pipelines_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).pipelines' do
+RSpec.describe 'Query.project(fullPath).pipelines', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb
index d49a4a7e768..0338b58a0ea 100644
--- a/spec/requests/api/graphql/ci/project_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/project_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).ciVariables' do
+RSpec.describe 'Query.project(fullPath).ciVariables', feature_category: :pipeline_authoring do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 94c0a3c41bd..ca08e780758 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.runner(id)' do
+RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
include GraphqlHelpers
let_it_be(:user) { create(:user, :admin) }
@@ -74,34 +74,39 @@ RSpec.describe 'Query.runner(id)' do
runner_data = graphql_data_at(:runner)
expect(runner_data).not_to be_nil
- expect(runner_data).to match a_hash_including(
- 'id' => runner.to_global_id.to_s,
- 'description' => runner.description,
- 'createdAt' => runner.created_at&.iso8601,
- 'contactedAt' => runner.contacted_at&.iso8601,
- 'version' => runner.version,
- 'shortSha' => runner.short_sha,
- 'revision' => runner.revision,
- 'locked' => false,
- 'active' => runner.active,
- 'paused' => !runner.active,
- 'status' => runner.status('14.5').to_s.upcase,
- 'maximumTimeout' => runner.maximum_timeout,
- 'accessLevel' => runner.access_level.to_s.upcase,
- 'runUntagged' => runner.run_untagged,
- 'ipAddress' => runner.ip_address,
- 'runnerType' => runner.instance_type? ? 'INSTANCE_TYPE' : 'PROJECT_TYPE',
- 'executorName' => runner.executor_type&.dasherize,
- 'architectureName' => runner.architecture,
- 'platformName' => runner.platform,
- 'maintenanceNote' => runner.maintenance_note,
- 'maintenanceNoteHtml' =>
+ expect(runner_data).to match a_graphql_entity_for(
+ runner,
+ description: runner.description,
+ created_at: runner.created_at&.iso8601,
+ contacted_at: runner.contacted_at&.iso8601,
+ version: runner.version,
+ short_sha: runner.short_sha,
+ revision: runner.revision,
+ locked: false,
+ active: runner.active,
+ paused: !runner.active,
+ status: runner.status('14.5').to_s.upcase,
+ job_execution_status: runner.builds.running.any? ? 'RUNNING' : 'IDLE',
+ maximum_timeout: runner.maximum_timeout,
+ access_level: runner.access_level.to_s.upcase,
+ run_untagged: runner.run_untagged,
+ ip_address: runner.ip_address,
+ runner_type: runner.instance_type? ? 'INSTANCE_TYPE' : 'PROJECT_TYPE',
+ executor_name: runner.executor_type&.dasherize,
+ architecture_name: runner.architecture,
+ platform_name: runner.platform,
+ maintenance_note: runner.maintenance_note,
+ maintenance_note_html:
runner.maintainer_note.present? ? a_string_including('<strong>Test maintenance note</strong>') : '',
- 'jobCount' => 0,
- 'jobs' => a_hash_including("count" => 0, "nodes" => [], "pageInfo" => anything),
- 'projectCount' => nil,
- 'adminUrl' => "http://localhost/admin/runners/#{runner.id}",
- 'userPermissions' => {
+ job_count: runner.builds.count,
+ jobs: a_hash_including(
+ "count" => runner.builds.count,
+ "nodes" => an_instance_of(Array),
+ "pageInfo" => anything
+ ),
+ project_count: nil,
+ admin_url: "http://localhost/admin/runners/#{runner.id}",
+ user_permissions: {
'readRunner' => true,
'updateRunner' => true,
'deleteRunner' => true,
@@ -129,10 +134,7 @@ RSpec.describe 'Query.runner(id)' do
runner_data = graphql_data_at(:runner)
expect(runner_data).not_to be_nil
- expect(runner_data).to match a_hash_including(
- 'id' => runner.to_global_id.to_s,
- 'adminUrl' => nil
- )
+ expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil)
expect(runner_data['tagList']).to match_array runner.tag_list
end
end
@@ -179,6 +181,16 @@ RSpec.describe 'Query.runner(id)' do
expect(runner_data).not_to include('tagList')
end
end
+
+ context 'with build running' do
+ before do
+ project = create(:project, :repository)
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, :running, runner: runner, pipeline: pipeline)
+ end
+
+ it_behaves_like 'runner details fetch'
+ end
end
describe 'for project runner' do
@@ -216,9 +228,47 @@ RSpec.describe 'Query.runner(id)' do
runner_data = graphql_data_at(:runner)
- expect(runner_data).to match a_hash_including(
- 'id' => project_runner.to_global_id.to_s,
- 'locked' => is_locked
+ expect(runner_data).to match a_graphql_entity_for(project_runner, locked: is_locked)
+ end
+ end
+ end
+
+ describe 'jobCount' do
+ let_it_be(:pipeline1) { create(:ci_pipeline, project: project1) }
+ let_it_be(:pipeline2) { create(:ci_pipeline, project: project1) }
+ let_it_be(:build1) { create(:ci_build, :running, runner: active_project_runner, pipeline: pipeline1) }
+ let_it_be(:build2) { create(:ci_build, :running, runner: active_project_runner, pipeline: pipeline2) }
+
+ let(:runner_query_fragment) { 'id jobCount' }
+ let(:query) do
+ %(
+ query {
+ runner1: runner(id: "#{active_project_runner.to_global_id}") { #{runner_query_fragment} }
+ runner2: runner(id: "#{inactive_instance_runner.to_global_id}") { #{runner_query_fragment} }
+ }
+ )
+ end
+
+ it 'retrieves correct jobCount values' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data).to match a_hash_including(
+ 'runner1' => a_graphql_entity_for(active_project_runner, job_count: 2),
+ 'runner2' => a_graphql_entity_for(inactive_instance_runner, job_count: 0)
+ )
+ end
+
+ context 'when JOB_COUNT_LIMIT is in effect' do
+ before do
+ stub_const('Types::Ci::RunnerType::JOB_COUNT_LIMIT', 0)
+ end
+
+ it 'retrieves correct capped jobCount values' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data).to match a_hash_including(
+ 'runner1' => a_graphql_entity_for(active_project_runner, job_count: 1),
+ 'runner2' => a_graphql_entity_for(inactive_instance_runner, job_count: 0)
)
end
end
@@ -243,18 +293,8 @@ RSpec.describe 'Query.runner(id)' do
post_graphql(query, current_user: user)
expect(graphql_data).to match a_hash_including(
- 'runner1' => {
- 'id' => runner1.to_global_id.to_s,
- 'ownerProject' => {
- 'id' => project2.to_global_id.to_s
- }
- },
- 'runner2' => {
- 'id' => runner2.to_global_id.to_s,
- 'ownerProject' => {
- 'id' => project1.to_global_id.to_s
- }
- }
+ 'runner1' => a_graphql_entity_for(runner1, owner_project: a_graphql_entity_for(project2)),
+ 'runner2' => a_graphql_entity_for(runner2, owner_project: a_graphql_entity_for(project1))
)
end
end
@@ -284,8 +324,8 @@ RSpec.describe 'Query.runner(id)' do
it 'retrieves groups field with expected value' do
post_graphql(query, current_user: user)
- runner_data = graphql_data_at(:runner, :groups)
- expect(runner_data).to eq 'nodes' => [{ 'id' => group.to_global_id.to_s }]
+ runner_data = graphql_data_at(:runner, :groups, :nodes)
+ expect(runner_data).to contain_exactly(a_graphql_entity_for(group))
end
end
@@ -409,13 +449,13 @@ RSpec.describe 'Query.runner(id)' do
'jobCount' => 1,
'jobs' => a_hash_including(
"count" => 1,
- "nodes" => [{ "id" => job.to_global_id.to_s, "status" => job.status.upcase }]
+ "nodes" => [a_graphql_entity_for(job, status: job.status.upcase)]
),
'projectCount' => 2,
'projects' => {
'nodes' => [
- { 'id' => project1.to_global_id.to_s },
- { 'id' => project2.to_global_id.to_s }
+ a_graphql_entity_for(project1),
+ a_graphql_entity_for(project2)
]
})
expect(runner2_data).to match a_hash_including(
@@ -486,15 +526,24 @@ RSpec.describe 'Query.runner(id)' do
groups {
nodes {
id
+ path
+ fullPath
+ webUrl
}
}
projects {
nodes {
id
+ path
+ fullPath
+ webUrl
}
}
ownerProject {
id
+ path
+ fullPath
+ webUrl
}
}
SINGLE
@@ -503,8 +552,8 @@ RSpec.describe 'Query.runner(id)' do
let(:active_project_runner2) { create(:ci_runner, :project) }
let(:active_group_runner2) { create(:ci_runner, :group) }
- # Currently excluding known N+1 issues, see https://gitlab.com/gitlab-org/gitlab/-/issues/334759
- let(:excluded_fields) { %w[jobCount groups projects ownerProject] }
+ # Exclude fields that are already hardcoded above
+ let(:excluded_fields) { %w[jobs groups projects ownerProject] }
let(:single_query) do
<<~QUERY
@@ -542,27 +591,98 @@ RSpec.describe 'Query.runner(id)' do
expect(graphql_data.count).to eq 6
expect(graphql_data).to match(
a_hash_including(
- 'instance_runner1' => a_hash_including('id' => active_instance_runner.to_global_id.to_s),
- 'instance_runner2' => a_hash_including('id' => inactive_instance_runner.to_global_id.to_s),
- 'group_runner1' => a_hash_including(
- 'id' => active_group_runner.to_global_id.to_s,
- 'groups' => { 'nodes' => [a_hash_including('id' => group.to_global_id.to_s)] }
+ 'instance_runner1' => a_graphql_entity_for(active_instance_runner),
+ 'instance_runner2' => a_graphql_entity_for(inactive_instance_runner),
+ 'group_runner1' => a_graphql_entity_for(
+ active_group_runner,
+ groups: { 'nodes' => contain_exactly(a_graphql_entity_for(group)) }
),
- 'group_runner2' => a_hash_including(
- 'id' => active_group_runner2.to_global_id.to_s,
- 'groups' => { 'nodes' => [a_hash_including('id' => active_group_runner2.groups[0].to_global_id.to_s)] }
+ 'group_runner2' => a_graphql_entity_for(
+ active_group_runner2,
+ groups: { 'nodes' => active_group_runner2.groups.map { |g| a_graphql_entity_for(g) } }
),
- 'project_runner1' => a_hash_including(
- 'id' => active_project_runner.to_global_id.to_s,
- 'projects' => { 'nodes' => [a_hash_including('id' => active_project_runner.projects[0].to_global_id.to_s)] },
- 'ownerProject' => a_hash_including('id' => active_project_runner.projects[0].to_global_id.to_s)
+ 'project_runner1' => a_graphql_entity_for(
+ active_project_runner,
+ projects: { 'nodes' => active_project_runner.projects.map { |p| a_graphql_entity_for(p) } },
+ owner_project: a_graphql_entity_for(active_project_runner.projects[0])
),
- 'project_runner2' => a_hash_including(
- 'id' => active_project_runner2.to_global_id.to_s,
- 'projects' => {
- 'nodes' => [a_hash_including('id' => active_project_runner2.projects[0].to_global_id.to_s)]
- },
- 'ownerProject' => a_hash_including('id' => active_project_runner2.projects[0].to_global_id.to_s)
+ 'project_runner2' => a_graphql_entity_for(
+ active_project_runner2,
+ projects: { 'nodes' => active_project_runner2.projects.map { |p| a_graphql_entity_for(p) } },
+ owner_project: a_graphql_entity_for(active_project_runner2.projects[0])
+ )
+ ))
+ end
+ end
+
+ describe 'Query limits with jobs' do
+ let!(:group1) { create(:group) }
+ let!(:group2) { create(:group) }
+ let!(:project1) { create(:project, :repository, group: group1) }
+ let!(:project2) { create(:project, :repository, group: group1) }
+ let!(:project3) { create(:project, :repository, group: group2) }
+
+ let!(:merge_request1) { create(:merge_request, source_project: project1) }
+ let!(:merge_request2) { create(:merge_request, source_project: project3) }
+
+ let(:project_runner2) { create(:ci_runner, :project, projects: [project1, project2]) }
+ let!(:build1) { create(:ci_build, :success, name: 'Build One', runner: project_runner2, pipeline: pipeline1) }
+ let!(:pipeline1) do
+ create(:ci_pipeline, project: project1, source: :merge_request_event, merge_request: merge_request1, ref: 'main',
+ target_sha: 'xxx')
+ end
+
+ let(:query) do
+ <<~QUERY
+ {
+ runner(id: "#{project_runner2.to_global_id}") {
+ id
+ jobs {
+ nodes {
+ id
+ detailedStatus {
+ id
+ detailsPath
+ group
+ icon
+ text
+ }
+ shortSha
+ commitPath
+ finishedAt
+ duration
+ queuedDuration
+ tags
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ it 'does not execute more queries per job', :aggregate_failures do
+ # warm-up license cache and so on:
+ personal_access_token = create(:personal_access_token, user: user)
+ args = { current_user: user, token: { personal_access_token: personal_access_token } }
+ post_graphql(query, **args)
+
+ control = ActiveRecord::QueryRecorder.new(query_recorder_debug: true) { post_graphql(query, **args) }
+
+ # Add a new build to project_runner2
+ project_runner2.runner_projects << build(:ci_runner_project, runner: project_runner2, project: project3)
+ pipeline2 = create(:ci_pipeline, project: project3, source: :merge_request_event, merge_request: merge_request2,
+ ref: 'main', target_sha: 'xxx')
+ build2 = create(:ci_build, :success, name: 'Build Two', runner: project_runner2, pipeline: pipeline2)
+
+ args[:current_user] = create(:user, :admin) # do not reuse same user
+ expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control)
+
+ expect(graphql_data.count).to eq 1
+ expect(graphql_data).to match(
+ a_hash_including(
+ 'runner' => a_graphql_entity_for(
+ project_runner2,
+ jobs: { 'nodes' => containing_exactly(a_graphql_entity_for(build1), a_graphql_entity_for(build2)) }
)
))
end
diff --git a/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb b/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb
index 767e958ea82..e84a1ca4cc4 100644
--- a/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_web_url_edge_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'RunnerWebUrlEdge' do
+RSpec.describe 'RunnerWebUrlEdge', feature_category: :runner_fleet do
include GraphqlHelpers
describe 'inside a Query.group' do
diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb
index 3054b866812..75d8609dc38 100644
--- a/spec/requests/api/graphql/ci/runners_spec.rb
+++ b/spec/requests/api/graphql/ci/runners_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Query.runners' do
+RSpec.describe 'Query.runners', feature_category: :runner_fleet do
include GraphqlHelpers
let_it_be(:current_user) { create_default(:user, :admin) }
@@ -48,7 +48,7 @@ RSpec.describe 'Query.runners' do
it_behaves_like 'a working graphql query'
it 'returns expected runner' do
- expect(runners_graphql_data['nodes'].map { |n| n['id'] }).to contain_exactly(expected_runner.to_global_id.to_s)
+ expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner))
end
end
diff --git a/spec/requests/api/graphql/ci/stages_spec.rb b/spec/requests/api/graphql/ci/stages_spec.rb
index 1edd6e58486..f4e1a69d455 100644
--- a/spec/requests/api/graphql/ci/stages_spec.rb
+++ b/spec/requests/api/graphql/ci/stages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Query.project.pipeline.stages' do
+RSpec.describe 'Query.project.pipeline.stages', feature_category: :continuous_integration do
include GraphqlHelpers
subject(:post_query) { post_graphql(query, current_user: user) }
diff --git a/spec/requests/api/graphql/ci/template_spec.rb b/spec/requests/api/graphql/ci/template_spec.rb
index 1bbef7d7f30..aaec219f734 100644
--- a/spec/requests/api/graphql/ci/template_spec.rb
+++ b/spec/requests/api/graphql/ci/template_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Querying CI template' do
+RSpec.describe 'Querying CI template', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :public) }
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 14c55e61a65..88f63fd59d7 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
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'container repository details' do
+RSpec.describe 'container repository details', feature_category: :container_registry do
include_context 'container registry tags'
include_context 'container registry client stubs'
diff --git a/spec/requests/api/graphql/crm/contacts_spec.rb b/spec/requests/api/graphql/crm/contacts_spec.rb
index a676e92dc3b..3ae19de63ed 100644
--- a/spec/requests/api/graphql/crm/contacts_spec.rb
+++ b/spec/requests/api/graphql/crm/contacts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting CRM contacts' do
+RSpec.describe 'getting CRM contacts', feature_category: :service_desk do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/current_user/groups_query_spec.rb b/spec/requests/api/graphql/current_user/groups_query_spec.rb
index 6e36beb2afc..151d07ff0a7 100644
--- a/spec/requests/api/graphql/current_user/groups_query_spec.rb
+++ b/spec/requests/api/graphql/current_user/groups_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query current user groups' do
+RSpec.describe 'Query current user groups', feature_category: :subgroups do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/current_user/todos_query_spec.rb b/spec/requests/api/graphql/current_user/todos_query_spec.rb
index 5a45f0db518..f7e23aeb241 100644
--- a/spec/requests/api/graphql/current_user/todos_query_spec.rb
+++ b/spec/requests/api/graphql/current_user/todos_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query current user todos' do
+RSpec.describe 'Query current user todos', feature_category: :source_code_management do
include GraphqlHelpers
include DesignManagementTestHelpers
@@ -19,7 +19,7 @@ RSpec.describe 'Query current user todos' do
let(:fields) do
<<~QUERY
nodes {
- #{all_graphql_fields_for('todos'.classify)}
+ #{all_graphql_fields_for('todos'.classify, max_depth: 2)}
}
QUERY
end
diff --git a/spec/requests/api/graphql/current_user_query_spec.rb b/spec/requests/api/graphql/current_user_query_spec.rb
index 086a57094ca..53d2580caee 100644
--- a/spec/requests/api/graphql/current_user_query_spec.rb
+++ b/spec/requests/api/graphql/current_user_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting project information' do
+RSpec.describe 'getting project information', feature_category: :authentication_and_authorization do
include GraphqlHelpers
let(:fields) do
diff --git a/spec/requests/api/graphql/current_user_todos_spec.rb b/spec/requests/api/graphql/current_user_todos_spec.rb
index da1c893ec2b..eaed51982e1 100644
--- a/spec/requests/api/graphql/current_user_todos_spec.rb
+++ b/spec/requests/api/graphql/current_user_todos_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'A Todoable that implements the CurrentUserTodos interface' do
+RSpec.describe 'A Todoable that implements the CurrentUserTodos interface',
+feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb
index 5dd5ad117b0..7b804623e01 100644
--- a/spec/requests/api/graphql/custom_emoji_query_spec.rb
+++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting custom emoji within namespace' do
+RSpec.describe 'getting custom emoji within namespace', feature_category: :not_owned do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/environments/deployments_query_spec.rb b/spec/requests/api/graphql/environments/deployments_spec.rb
index 6da00057449..0022a38d2d3 100644
--- a/spec/requests/api/graphql/environments/deployments_query_spec.rb
+++ b/spec/requests/api/graphql/environments/deployments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Environments Deployments query' do
+RSpec.describe 'Environments Deployments query', feature_category: :continuous_delivery do
include GraphqlHelpers
let_it_be(:project) { create(:project, :private, :repository) }
@@ -437,6 +437,43 @@ RSpec.describe 'Environments Deployments query' do
end
end
+ context 'when requesting user permissions' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments {
+ nodes {
+ iid
+ userPermissions {
+ updateDeployment
+ destroyDeployment
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it_behaves_like 'avoids N+1 database queries'
+
+ it 'returns user permissions of the deployments', :aggregate_failures do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ deployments.each do |deployment|
+ deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
+
+ expect(deployment['userPermissions']['updateDeployment'])
+ .to eq(Ability.allowed?(user, :update_deployment, deployment_in_record))
+ expect(deployment['userPermissions']['destroyDeployment'])
+ .to eq(Ability.allowed?(user, :destroy_deployment, deployment_in_record))
+ end
+ end
+ end
+
describe 'sorting and pagination' do
let(:data_path) { [:project, :environment, :deployments] }
let(:current_user) { user }
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index c1beadb6c45..7937091ea7c 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'GitlabSchema configurations' do
+RSpec.describe 'GitlabSchema configurations', feature_category: :not_owned do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb
index 8ec321c8d7c..51d12261247 100644
--- a/spec/requests/api/graphql/group/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/group/container_repositories_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting container repositories in a group' do
+RSpec.describe 'getting container repositories in a group', feature_category: :source_code_management do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb
index daa1483e956..2c4770a31a7 100644
--- a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb
+++ b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting dependency proxy blobs in a group' do
+RSpec.describe 'getting dependency proxy blobs in a group', feature_category: :dependency_proxy do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb
index cc706c3051f..aca8527ba0a 100644
--- a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb
+++ b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting dependency proxy settings for a group' do
+RSpec.describe 'getting dependency proxy settings for a group', feature_category: :dependency_proxy do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb
index 3b2b04b1322..edff4dc1dae 100644
--- a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb
+++ b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting dependency proxy image ttl policy for a group' do
+RSpec.describe 'getting dependency proxy image ttl policy for a group', feature_category: :dependency_proxy do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb
index 37ef7089c2f..d2d686104ad 100644
--- a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb
+++ b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting dependency proxy manifests in a group' do
+RSpec.describe 'getting dependency proxy manifests in a group', feature_category: :dependency_proxy do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb
index 5f8becc0726..26d1fb48408 100644
--- a/spec/requests/api/graphql/group/group_members_spec.rb
+++ b/spec/requests/api/graphql/group/group_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting group members information' do
+RSpec.describe 'getting group members information', feature_category: :subgroups do
include GraphqlHelpers
let_it_be(:parent_group) { create(:group, :public) }
diff --git a/spec/requests/api/graphql/group/issues_spec.rb b/spec/requests/api/graphql/group/issues_spec.rb
index 26338f46611..95aeed32558 100644
--- a/spec/requests/api/graphql/group/issues_spec.rb
+++ b/spec/requests/api/graphql/group/issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting an issue list for a group' do
+RSpec.describe 'getting an issue list for a group', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/group/labels_query_spec.rb b/spec/requests/api/graphql/group/labels_query_spec.rb
index 31556ffca30..28886f8d80b 100644
--- a/spec/requests/api/graphql/group/labels_query_spec.rb
+++ b/spec/requests/api/graphql/group/labels_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting group label information' do
+RSpec.describe 'getting group label information', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:group) { create(:group, :public) }
diff --git a/spec/requests/api/graphql/group/merge_requests_spec.rb b/spec/requests/api/graphql/group/merge_requests_spec.rb
index 434b0d16569..6976685ecc0 100644
--- a/spec/requests/api/graphql/group/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/group/merge_requests_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
# Based on ee/spec/requests/api/epics_spec.rb
# Should follow closely in order to ensure all situations are covered
-RSpec.describe 'Query.group.mergeRequests' do
+RSpec.describe 'Query.group.mergeRequests', feature_category: :code_review do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb
index 7c51409f907..28cd68493c0 100644
--- a/spec/requests/api/graphql/group/milestones_spec.rb
+++ b/spec/requests/api/graphql/group/milestones_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Milestones through GroupQuery' do
+RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/group/packages_spec.rb b/spec/requests/api/graphql/group/packages_spec.rb
index cf8736db5af..0b4057c87f8 100644
--- a/spec/requests/api/graphql/group/packages_spec.rb
+++ b/spec/requests/api/graphql/group/packages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting a package list for a group' do
+RSpec.describe 'getting a package list for a group', feature_category: :package_registry do
include GraphqlHelpers
let_it_be(:resource) { create(:group, :private) }
diff --git a/spec/requests/api/graphql/group/recent_issue_boards_query_spec.rb b/spec/requests/api/graphql/group/recent_issue_boards_query_spec.rb
index 4914beec870..2dfbc95bac9 100644
--- a/spec/requests/api/graphql/group/recent_issue_boards_query_spec.rb
+++ b/spec/requests/api/graphql/group/recent_issue_boards_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting group recent issue boards' do
+RSpec.describe 'getting group recent issue boards', feature_category: :team_planning do
include GraphqlHelpers
it_behaves_like 'querying a GraphQL type recent boards' do
diff --git a/spec/requests/api/graphql/group/timelogs_spec.rb b/spec/requests/api/graphql/group/timelogs_spec.rb
index 05b6ee3ff89..b67b39edff2 100644
--- a/spec/requests/api/graphql/group/timelogs_spec.rb
+++ b/spec/requests/api/graphql/group/timelogs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Timelogs through GroupQuery' do
+RSpec.describe 'Timelogs through GroupQuery', feature_category: :team_planning do
include GraphqlHelpers
describe 'Get list of timelogs from a group issues' do
diff --git a/spec/requests/api/graphql/group/work_item_types_spec.rb b/spec/requests/api/graphql/group/work_item_types_spec.rb
index 35090e2a89f..791c0fb9524 100644
--- a/spec/requests/api/graphql/group/work_item_types_spec.rb
+++ b/spec/requests/api/graphql/group/work_item_types_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting a list of work item types for a group' do
+RSpec.describe 'getting a list of work item types for a group', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:developer) { create(:user) }
diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb
index 8ee5c3c5d73..bc288c0a98b 100644
--- a/spec/requests/api/graphql/group_query_spec.rb
+++ b/spec/requests/api/graphql/group_query_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
# Based on spec/requests/api/groups_spec.rb
# Should follow closely in order to ensure all situations are covered
-RSpec.describe 'getting group information' do
+RSpec.describe 'getting group information', feature_category: :subgroups do
include GraphqlHelpers
include UploadHelpers
diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb
index 6e2d736f244..101de692aa5 100644
--- a/spec/requests/api/graphql/issue/issue_spec.rb
+++ b/spec/requests/api/graphql/issue/issue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.issue(id)' do
+RSpec.describe 'Query.issue(id)', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/issue_status_counts_spec.rb b/spec/requests/api/graphql/issue_status_counts_spec.rb
index 89ecbf44b10..72a1968cb27 100644
--- a/spec/requests/api/graphql/issue_status_counts_spec.rb
+++ b/spec/requests/api/graphql/issue_status_counts_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting Issue counts by status' do
+RSpec.describe 'getting Issue counts by status', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/requests/api/graphql/issues_spec.rb b/spec/requests/api/graphql/issues_spec.rb
index 8838ad78f72..ba6f8ec2cab 100644
--- a/spec/requests/api/graphql/issues_spec.rb
+++ b/spec/requests/api/graphql/issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting an issue list at root level' do
+RSpec.describe 'getting an issue list at root level', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:developer) { create(:user) }
@@ -13,34 +13,81 @@ RSpec.describe 'getting an issue list at root level' do
let_it_be(:project_b) { create(:project, :repository, :private, group: group1) }
let_it_be(:project_c) { create(:project, :repository, :public, group: group2) }
let_it_be(:project_d) { create(:project, :repository, :private, group: group2) }
- let_it_be(:early_milestone) { create(:milestone, project: project_d, due_date: 10.days.from_now) }
- let_it_be(:late_milestone) { create(:milestone, project: project_c, due_date: 30.days.from_now) }
+ let_it_be(:milestone1) { create(:milestone, project: project_c, due_date: 10.days.from_now) }
+ let_it_be(:milestone2) { create(:milestone, project: project_d, due_date: 20.days.from_now) }
+ let_it_be(:milestone3) { create(:milestone, project: project_d, due_date: 30.days.from_now) }
+ let_it_be(:milestone4) { create(:milestone, project: project_a, due_date: 40.days.from_now) }
let_it_be(:priority1) { create(:label, project: project_c, priority: 1) }
let_it_be(:priority2) { create(:label, project: project_d, priority: 5) }
let_it_be(:priority3) { create(:label, project: project_a, priority: 10) }
+ let_it_be(:priority4) { create(:label, project: project_d, priority: 15) }
+
+ let_it_be(:issue_a) do
+ create(
+ :issue,
+ project: project_a,
+ labels: [priority3],
+ due_date: 1.day.ago,
+ milestone: milestone4,
+ relative_position: 1000
+ )
+ end
+
+ let_it_be(:issue_b) do
+ create(
+ :issue,
+ :with_alert,
+ project: project_b,
+ discussion_locked: true,
+ due_date: 1.day.from_now,
+ relative_position: 3000
+ )
+ end
- let_it_be(:issue_a) { create(:issue, project: project_a, labels: [priority3]) }
- let_it_be(:issue_b) { create(:issue, :with_alert, project: project_b, discussion_locked: true) }
let_it_be(:issue_c) do
create(
:issue,
+ :confidential,
project: project_c,
title: 'title matching issue plus',
labels: [priority1],
- milestone: late_milestone
+ milestone: milestone1,
+ due_date: 3.days.from_now,
+ relative_position: nil
)
end
- let_it_be(:issue_d) { create(:issue, :with_alert, project: project_d, discussion_locked: true, labels: [priority2]) }
- let_it_be(:issue_e) { create(:issue, project: project_d, milestone: early_milestone) }
+ let_it_be(:issue_d) do
+ create(
+ :issue,
+ :with_alert,
+ project: project_d,
+ discussion_locked: true,
+ labels: [priority2],
+ milestone: milestone3,
+ relative_position: 5000
+ )
+ end
- let(:issue_filter_params) { {} }
+ let_it_be(:issue_e) do
+ create(
+ :issue,
+ :confidential,
+ project: project_d,
+ milestone: milestone2,
+ due_date: 3.days.ago,
+ relative_position: nil,
+ labels: [priority2, priority4]
+ )
+ end
+ let_it_be(:issues, reload: true) { [issue_a, issue_b, issue_c, issue_d, issue_e] }
+
+ let(:issue_filter_params) { {} }
+ let(:current_user) { developer }
let(:fields) do
<<~QUERY
- nodes {
- #{all_graphql_fields_for('issues'.classify)}
- }
+ nodes { id }
QUERY
end
@@ -60,13 +107,16 @@ RSpec.describe 'getting an issue list at root level' do
end
end
+ # All new specs should be added to the shared example if the change also
+ # affects the `issues` query at the root level of the API.
+ # Shared example also used in spec/requests/api/graphql/project/issues_spec.rb
it_behaves_like 'graphql issue list request spec' do
- subject(:post_query) { post_graphql(query, current_user: current_user) }
+ let_it_be(:external_user) { create(:user) }
+
+ let(:public_projects) { [project_a, project_c] }
- let(:current_user) { developer }
let(:another_user) { reporter }
- let(:issues_data) { graphql_data['issues']['nodes'] }
- let(:issue_ids) { graphql_dig_at(issues_data, :id) }
+ let(:issue_nodes_path) { %w[issues nodes] }
# filters
let(:expected_negated_assignee_issues) { [issue_b, issue_c, issue_d, issue_e] }
@@ -77,12 +127,25 @@ RSpec.describe 'getting an issue list at root level' do
let(:unlocked_discussion_issues) { [issue_a, issue_c, issue_e] }
let(:search_title_term) { 'matching issue' }
let(:title_search_issue) { issue_c }
+ let(:confidential_issues) { [issue_c, issue_e] }
+ let(:non_confidential_issues) { [issue_a, issue_b, issue_d] }
+ let(:public_non_confidential_issues) { [issue_a] }
# sorting
let(:data_path) { [:issues] }
- let(:expected_severity_sorted_asc) { [issue_c, issue_a, issue_b, issue_e, issue_d] }
- let(:expected_priority_sorted_asc) { [issue_e, issue_c, issue_d, issue_a, issue_b] }
- let(:expected_priority_sorted_desc) { [issue_c, issue_e, issue_a, issue_d, issue_b] }
+ let(:expected_priority_sorted_asc) { [issue_c, issue_e, issue_d, issue_a, issue_b] }
+ let(:expected_priority_sorted_desc) { [issue_a, issue_d, issue_e, issue_c, issue_b] }
+ let(:expected_due_date_sorted_desc) { [issue_c, issue_b, issue_a, issue_e, issue_d] }
+ let(:expected_due_date_sorted_asc) { [issue_e, issue_a, issue_b, issue_c, issue_d] }
+ let(:expected_relative_position_sorted_asc) { [issue_a, issue_b, issue_d, issue_c, issue_e] }
+ let(:expected_label_priority_sorted_asc) { [issue_c, issue_e, issue_d, issue_a, issue_b] }
+ let(:expected_label_priority_sorted_desc) { [issue_a, issue_e, issue_d, issue_c, issue_b] }
+ let(:expected_milestone_sorted_asc) { [issue_c, issue_e, issue_d, issue_a, issue_b] }
+ let(:expected_milestone_sorted_desc) { [issue_a, issue_d, issue_e, issue_c, issue_b] }
+
+ # N+1 queries
+ let(:same_project_issue1) { issue_d }
+ let(:same_project_issue2) { issue_e }
before_all do
issue_a.assignee_ids = developer.id
@@ -90,12 +153,6 @@ RSpec.describe 'getting an issue list at root level' do
create(:award_emoji, :upvote, user: developer, awardable: issue_a)
create(:award_emoji, :upvote, user: developer, awardable: issue_c)
-
- # severity sorting
- create(:issuable_severity, issue: issue_a, severity: :unknown)
- create(:issuable_severity, issue: issue_b, severity: :low)
- create(:issuable_severity, issue: issue_d, severity: :critical)
- create(:issuable_severity, issue: issue_e, severity: :high)
end
def pagination_query(params)
@@ -107,6 +164,27 @@ RSpec.describe 'getting an issue list at root level' do
end
end
+ context 'when fetching issues from multiple projects' do
+ it 'avoids N+1 queries' do
+ post_query # warm-up
+
+ control = ActiveRecord::QueryRecorder.new { post_query }
+
+ new_private_project = create(:project, :private).tap { |project| project.add_developer(current_user) }
+ create(:issue, project: new_private_project)
+
+ expect { post_query }.not_to exceed_query_limit(control)
+ end
+ end
+
+ def execute_query
+ post_query
+ end
+
+ def post_query(request_user = current_user)
+ post_graphql(query, current_user: request_user)
+ end
+
def query(params = issue_filter_params)
graphql_query_for(
:issues,
diff --git a/spec/requests/api/graphql/jobs_query_spec.rb b/spec/requests/api/graphql/jobs_query_spec.rb
index 5907566be7f..0aea8e4c253 100644
--- a/spec/requests/api/graphql/jobs_query_spec.rb
+++ b/spec/requests/api/graphql/jobs_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting job information' do
+RSpec.describe 'getting job information', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:job) { create(:ci_build, :success, name: 'job1') }
diff --git a/spec/requests/api/graphql/merge_request/merge_request_spec.rb b/spec/requests/api/graphql/merge_request/merge_request_spec.rb
index d89f381753e..213697bacc1 100644
--- a/spec/requests/api/graphql/merge_request/merge_request_spec.rb
+++ b/spec/requests/api/graphql/merge_request/merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.merge_request(id)' do
+RSpec.describe 'Query.merge_request(id)', feature_category: :code_review do
include GraphqlHelpers
let_it_be(:project) { create(:project, :empty_repo) }
diff --git a/spec/requests/api/graphql/metadata_query_spec.rb b/spec/requests/api/graphql/metadata_query_spec.rb
index 435e1b5b596..7d1850b1b93 100644
--- a/spec/requests/api/graphql/metadata_query_spec.rb
+++ b/spec/requests/api/graphql/metadata_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting project information' do
+RSpec.describe 'getting project information', feature_category: :projects do
include GraphqlHelpers
let(:query) { graphql_query_for('metadata', {}, all_graphql_fields_for('Metadata')) }
diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
index 72ec2b8e070..4dd47142c40 100644
--- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Getting Metrics Dashboard Annotations' do
+RSpec.describe 'Getting Metrics Dashboard Annotations', feature_category: :metrics do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
index 1b84acff0e2..8db0844c6d7 100644
--- a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Getting Metrics Dashboard' do
+RSpec.describe 'Getting Metrics Dashboard', feature_category: :metrics do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
@@ -26,156 +26,73 @@ RSpec.describe 'Getting Metrics Dashboard' do
)
end
- context 'with metrics_dashboard_exhaustive_validations feature flag off' do
+ context 'for anonymous user' do
before do
- stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
+ post_graphql(query, current_user: current_user)
end
- context 'for anonymous user' do
- before do
- post_graphql(query, current_user: current_user)
- end
-
- context 'requested dashboard is available' do
- let(:path) { 'config/prometheus/common_metrics.yml' }
-
- it_behaves_like 'a working graphql query'
-
- it 'returns nil' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes')
-
- expect(dashboard).to be_nil
- end
- end
- end
-
- context 'for user with developer access' do
- before do
- project.add_developer(current_user)
- post_graphql(query, current_user: current_user)
- end
-
- context 'requested dashboard is available' do
- let(:path) { 'config/prometheus/common_metrics.yml' }
-
- it_behaves_like 'a working graphql query'
-
- it 'returns metrics dashboard' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
-
- expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
- end
-
- context 'invalid dashboard' do
- let(:path) { '.gitlab/dashboards/metrics.yml' }
- let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) }
-
- it 'returns metrics dashboard' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
-
- expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["panel_groups: should be an array of panel_groups objects"])
- end
- end
-
- context 'empty dashboard' do
- let(:path) { '.gitlab/dashboards/metrics.yml' }
- let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) }
-
- it 'returns metrics dashboard' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
+ context 'requested dashboard is available' do
+ let(:path) { 'config/prometheus/common_metrics.yml' }
- expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"])
- end
- end
- end
-
- context 'requested dashboard can not be found' do
- let(:path) { 'config/prometheus/i_am_not_here.yml' }
+ it_behaves_like 'a working graphql query'
- it_behaves_like 'a working graphql query'
+ it 'returns nil' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes')
- it 'returns nil' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
-
- expect(dashboard).to be_nil
- end
+ expect(dashboard).to be_nil
end
end
end
- context 'with metrics_dashboard_exhaustive_validations feature flag on' do
+ context 'for user with developer access' do
before do
- stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
+ project.add_developer(current_user)
+ post_graphql(query, current_user: current_user)
end
- context 'for anonymous user' do
- before do
- post_graphql(query, current_user: current_user)
- end
-
- context 'requested dashboard is available' do
- let(:path) { 'config/prometheus/common_metrics.yml' }
-
- it_behaves_like 'a working graphql query'
+ context 'requested dashboard is available' do
+ let(:path) { 'config/prometheus/common_metrics.yml' }
- it 'returns nil' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes')
+ it_behaves_like 'a working graphql query'
- expect(dashboard).to be_nil
- end
- end
- end
+ it 'returns metrics dashboard' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
- context 'for user with developer access' do
- before do
- project.add_developer(current_user)
- post_graphql(query, current_user: current_user)
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
end
- context 'requested dashboard is available' do
- let(:path) { 'config/prometheus/common_metrics.yml' }
-
- it_behaves_like 'a working graphql query'
+ context 'invalid dashboard' do
+ let(:path) { '.gitlab/dashboards/metrics.yml' }
+ let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) }
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
- expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
- end
-
- context 'invalid dashboard' do
- let(:path) { '.gitlab/dashboards/metrics.yml' }
- let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) }
-
- it 'returns metrics dashboard' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
-
- expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["root is missing required keys: panel_groups"])
- end
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["panel_groups: should be an array of panel_groups objects"])
end
+ end
- context 'empty dashboard' do
- let(:path) { '.gitlab/dashboards/metrics.yml' }
- let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) }
+ context 'empty dashboard' do
+ let(:path) { '.gitlab/dashboards/metrics.yml' }
+ let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) }
- it 'returns metrics dashboard' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
+ it 'returns metrics dashboard' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
- expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["root is missing required keys: dashboard, panel_groups"])
- end
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"])
end
end
+ end
- context 'requested dashboard can not be found' do
- let(:path) { 'config/prometheus/i_am_not_here.yml' }
+ context 'requested dashboard can not be found' do
+ let(:path) { 'config/prometheus/i_am_not_here.yml' }
- it_behaves_like 'a working graphql query'
+ it_behaves_like 'a working graphql query'
- it 'returns nil' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
+ it 'returns nil' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
- expect(dashboard).to be_nil
- end
+ expect(dashboard).to be_nil
end
end
end
diff --git a/spec/requests/api/graphql/milestone_spec.rb b/spec/requests/api/graphql/milestone_spec.rb
index 78e7ec39ee3..2cea9fd0408 100644
--- a/spec/requests/api/graphql/milestone_spec.rb
+++ b/spec/requests/api/graphql/milestone_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Querying a Milestone' do
+RSpec.describe 'Querying a Milestone', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:group) { create(:group, :public) }
diff --git a/spec/requests/api/graphql/multiplexed_queries_spec.rb b/spec/requests/api/graphql/multiplexed_queries_spec.rb
index f79bac6ae3b..4d615d3eaa4 100644
--- a/spec/requests/api/graphql/multiplexed_queries_spec.rb
+++ b/spec/requests/api/graphql/multiplexed_queries_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Multiplexed queries' do
+RSpec.describe 'Multiplexed queries', feature_category: :not_owned do
include GraphqlHelpers
it 'returns responses for multiple queries' do
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 f992e46879f..64ea6d32f5f 100644
--- a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
+++ b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues do
+RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_category: :not_owned do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
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 f637ca98353..fbe6d95dfff 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create an alert issue from an alert' do
+RSpec.describe 'Create an alert issue from an alert', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb
index fcef7b4e3ec..935856814c4 100644
--- a/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting assignees of an alert' do
+RSpec.describe 'Setting assignees of an alert', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb
index 48307964345..570324a3126 100644
--- a/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creating a todo for the alert' do
+RSpec.describe 'Creating a todo for the alert', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb
index 802d8d6c5a1..6537747850c 100644
--- a/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting the status of an alert' do
+RSpec.describe 'Setting the status of an alert', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb
index ff93da2153f..187c88363c6 100644
--- a/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creating a new HTTP Integration' do
+RSpec.describe 'Creating a new HTTP Integration', feature_category: :integrations do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb
index 1ecb5c76b57..1c77c71daba 100644
--- a/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Removing an HTTP Integration' do
+RSpec.describe 'Removing an HTTP Integration', feature_category: :integrations do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb
index badd9412589..427277dd540 100644
--- a/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Resetting a token on an existing HTTP Integration' do
+RSpec.describe 'Resetting a token on an existing HTTP Integration', feature_category: :integrations do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb
index 18cbb7d8b00..a9d189d564d 100644
--- a/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Updating an existing HTTP Integration' do
+RSpec.describe 'Updating an existing HTTP Integration', feature_category: :integrations do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
index 4c359d9b357..3dee7f50af3 100644
--- a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creating a new Prometheus Integration' do
+RSpec.describe 'Creating a new Prometheus Integration', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb
index 31053c50cac..15127843b95 100644
--- a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Resetting a token on an existing Prometheus Integration' do
+RSpec.describe 'Resetting a token on an existing Prometheus Integration', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb
index ad26ec118d7..63e95f4513b 100644
--- a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Updating an existing Prometheus Integration' do
+RSpec.describe 'Updating an existing Prometheus Integration', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
index 3879e58cecf..fdbff0f93cd 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Adding an AwardEmoji' do
+RSpec.describe 'Adding an AwardEmoji', feature_category: :not_owned do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
index e81621209fb..e200bfc2d18 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Removing an AwardEmoji' do
+RSpec.describe 'Removing an AwardEmoji', feature_category: :not_owned do
include GraphqlHelpers
let(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
index b151da72b55..6dba2b58357 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Toggling an AwardEmoji' do
+RSpec.describe 'Toggling an AwardEmoji', feature_category: :not_owned do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/boards/create_spec.rb b/spec/requests/api/graphql/mutations/boards/create_spec.rb
index ca848c0c92f..10eb12f277f 100644
--- a/spec/requests/api/graphql/mutations/boards/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::Boards::Create do
+RSpec.describe Mutations::Boards::Create, feature_category: :team_planning do
let_it_be(:parent) { create(:project) }
let_it_be(:current_user, reload: true) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/boards/destroy_spec.rb b/spec/requests/api/graphql/mutations/boards/destroy_spec.rb
index 7620da3e7e0..44924159137 100644
--- a/spec/requests/api/graphql/mutations/boards/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::Boards::Destroy do
+RSpec.describe Mutations::Boards::Destroy, feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user, reload: true) { create(:user) }
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 06093e9f7c2..df64caa1cfb 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Reposition and move issue within board lists' do
+RSpec.describe 'Reposition and move issue within board lists', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:group) { create(:group, :private) }
diff --git a/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb
index fec9a8c6307..f5381be2741 100644
--- a/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a label or backlog board list' do
+RSpec.describe 'Create a label or backlog board list', feature_category: :team_planning do
let_it_be(:group) { create(:group, :private) }
let_it_be(:board) { create(:board, group: group) }
diff --git a/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb
index 83309ead352..b37b1a7b935 100644
--- a/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::Boards::Lists::Destroy do
+RSpec.describe Mutations::Boards::Lists::Destroy, feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user, reload: true) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb
index c7885879a9d..6a7edcd7349 100644
--- a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Update of an existing board list' do
+RSpec.describe 'Update of an existing board list', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/branches/create_spec.rb b/spec/requests/api/graphql/mutations/branches/create_spec.rb
index 9ee2f41e8fc..32512e2ee1b 100644
--- a/spec/requests/api/graphql/mutations/branches/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/branches/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creation of a new branch' do
+RSpec.describe 'Creation of a new branch', feature_category: :source_code_management do
include GraphqlHelpers
let_it_be(:group) { create(:group, :public) }
diff --git a/spec/requests/api/graphql/mutations/ci/job/artifacts_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job/artifacts_destroy_spec.rb
index bdad80995ea..6cdf8788957 100644
--- a/spec/requests/api/graphql/mutations/ci/job/artifacts_destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/artifacts_destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'JobArtifactsDestroy' do
+RSpec.describe 'JobArtifactsDestroy', feature_category: :build_artifacts do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb
index 5855eb6bb51..88dfec41d36 100644
--- a/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'JobArtifactsDestroy' do
+RSpec.describe 'JobArtifactsDestroy', feature_category: :build_artifacts do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb
index a5ec9ea343d..ac3592130b8 100644
--- a/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'ArtifactDestroy' do
+RSpec.describe 'ArtifactDestroy', feature_category: :build_artifacts do
include GraphqlHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb
index ee0f0a9bccb..468a9e57f56 100644
--- a/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "JobCancel" do
+RSpec.describe "JobCancel", feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/job_play_spec.rb b/spec/requests/api/graphql/mutations/ci/job_play_spec.rb
index 0874e225259..014a5e0f1c7 100644
--- a/spec/requests/api/graphql/mutations/ci/job_play_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_play_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'JobPlay' do
+RSpec.describe 'JobPlay', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb
index 8cf559a372a..e49ee6f3163 100644
--- a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'JobRetry' do
+RSpec.describe 'JobRetry', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
index b2f84ab2869..490716ddbe2 100644
--- a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'CiJobTokenScopeAddProject' do
+RSpec.describe 'CiJobTokenScopeAddProject', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) }
@@ -60,7 +60,7 @@ RSpec.describe 'CiJobTokenScopeAddProject' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
- end.to change { Ci::JobToken::Scope.new(project).includes?(target_project) }.from(false).to(true)
+ end.to change { Ci::JobToken::Scope.new(project).allows?(target_project) }.from(false).to(true)
end
context 'when invalid target project is provided' do
diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
index 2b0adf89f40..607c6bd85c2 100644
--- a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'CiJobTokenScopeRemoveProject' do
+RSpec.describe 'CiJobTokenScopeRemoveProject', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) }
@@ -66,7 +66,7 @@ RSpec.describe 'CiJobTokenScopeRemoveProject' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
- end.to change { Ci::JobToken::Scope.new(project).includes?(target_project) }.from(true).to(false)
+ end.to change { Ci::JobToken::Scope.new(project).allows?(target_project) }.from(true).to(false)
end
context 'when invalid target project is provided' do
diff --git a/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb b/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb
index 4ddc019a2b5..6868b0ea279 100644
--- a/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'JobUnschedule' do
+RSpec.describe 'JobUnschedule', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb
index 6ec1b7ce9b6..8c1359384ed 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'PipelineCancel' do
+RSpec.describe 'PipelineCancel', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb
index 7abd5ca8772..9ddfaf83d34 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'PipelineDestroy' do
+RSpec.describe 'PipelineDestroy', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
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 f6acf29c321..e7edc86bea0 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'PipelineRetry' do
+RSpec.describe 'PipelineRetry', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb
new file mode 100644
index 00000000000..4a45d255d99
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_create_spec.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'PipelineSchedulecreate' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ **pipeline_schedule_parameters
+ }
+
+ graphql_mutation(
+ :pipeline_schedule_create,
+ variables,
+ <<-QL
+ pipelineSchedule {
+ id
+ description
+ cron
+ refForDisplay
+ active
+ cronTimezone
+ variables {
+ nodes {
+ key
+ value
+ }
+ }
+ owner {
+ id
+ }
+ }
+ errors
+ QL
+ )
+ end
+
+ let(:pipeline_schedule_parameters) do
+ {
+ description: 'created_desc',
+ cron: '0 1 * * *',
+ cronTimezone: 'UTC',
+ ref: 'patch-x',
+ active: true,
+ variables: [
+ { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' }
+ ]
+ }
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:pipeline_schedule_create) }
+
+ context 'when unauthorized' do
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).not_to be_empty
+ expect(graphql_errors[0]['message'])
+ .to eq(
+ "The resource that you are attempting to access does not exist " \
+ "or you don't have permission to perform this action"
+ )
+ end
+ end
+
+ context 'when authorized' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when success' do
+ it do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect(mutation_response['pipelineSchedule']['owner']['id']).to eq(user.to_global_id.to_s)
+
+ %w[description cron cronTimezone active].each do |key|
+ expect(mutation_response['pipelineSchedule'][key]).to eq(pipeline_schedule_parameters[key.to_sym])
+ end
+
+ expect(mutation_response['pipelineSchedule']['refForDisplay']).to eq(pipeline_schedule_parameters[:ref])
+
+ expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['key']).to eq('AAA')
+ expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['value']).to eq('AAA123')
+
+ expect(mutation_response['pipelineSchedule']['owner']['id']).to eq(user.to_global_id.to_s)
+
+ expect(mutation_response['errors']).to eq([])
+ end
+ end
+
+ context 'when failure' do
+ context 'when params are invalid' do
+ let(:pipeline_schedule_parameters) do
+ {
+ description: 'some description',
+ cron: 'abc',
+ cronTimezone: 'cCc',
+ ref: 'asd',
+ active: true,
+ variables: []
+ }
+ end
+
+ it do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect(mutation_response['errors'])
+ .to match_array(
+ ["Cron is invalid syntax", "Cron timezone is invalid syntax"]
+ )
+ end
+ end
+
+ context 'when variables have duplicate name' do
+ before do
+ pipeline_schedule_parameters.merge!(
+ {
+ variables: [
+ { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' },
+ { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' }
+ ]
+ }
+ )
+ end
+
+ it 'returns error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect(mutation_response['errors'])
+ .to match_array(
+ [
+ "Variables have duplicate values (AAA)"
+ ]
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb
index b197d223463..b846ff0aec8 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'PipelineScheduleDelete' do
+RSpec.describe 'PipelineScheduleDelete', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb
new file mode 100644
index 00000000000..0e43fa024f3
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'PipelineSchedulePlay', feature_category: :continuious_integration do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline_schedule) do
+ create(
+ :ci_pipeline_schedule,
+ :every_minute,
+ project: project,
+ owner: user
+ )
+ end
+
+ let(:mutation) do
+ graphql_mutation(
+ :pipeline_schedule_play,
+ { id: pipeline_schedule.to_global_id.to_s },
+ <<-QL
+ pipelineSchedule { id, nextRunAt }
+ errors
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:pipeline_schedule_play) }
+
+ context 'when unauthorized' do
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ expect(graphql_errors[0]['message'])
+ .to eq(
+ "The resource that you are attempting to access does not exist " \
+ "or you don't have permission to perform this action"
+ )
+ end
+ end
+
+ context 'when authorized' do
+ before do
+ project.add_maintainer(user)
+ pipeline_schedule.update_columns(next_run_at: 2.hours.ago)
+ end
+
+ context 'when mutation succeeds' do
+ it do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(mutation_response['pipelineSchedule']['id']).to include(pipeline_schedule.id.to_s)
+ new_next_run_at = DateTime.parse(mutation_response['pipelineSchedule']['nextRunAt'])
+ expect(new_next_run_at).not_to eq(pipeline_schedule.next_run_at)
+ expect(new_next_run_at).to eq(pipeline_schedule.reset.next_run_at)
+ expect(mutation_response['errors']).to eq([])
+ end
+ end
+
+ context 'when mutation fails' do
+ before do
+ allow(RunPipelineScheduleWorker).to receive(:perform_async).and_return(nil)
+ end
+
+ it do
+ expect(RunPipelineScheduleWorker)
+ .to receive(:perform_async)
+ .with(pipeline_schedule.id, user.id)
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(mutation_response['pipelineSchedule']).to be_nil
+ expect(mutation_response['errors']).to match_array(['Unable to schedule a pipeline to run immediately.'])
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb
index 8dfbf20d00b..2d1f1565a73 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'PipelineScheduleTakeOwnership' do
+RSpec.describe 'PipelineScheduleTakeOwnership', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
index c808cf5ede9..7a6ee7c2ecc 100644
--- a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'ProjectCiCdSettingsUpdate' do
+RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) do
diff --git a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
index 54e63df96a6..752242c3ab3 100644
--- a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'RunnersRegistrationTokenReset' do
+RSpec.describe 'RunnersRegistrationTokenReset', feature_category: :runner_fleet do
include GraphqlHelpers
let(:mutation) { graphql_mutation(:runners_registration_token_reset, input) }
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 aac8eb22771..f544cef8864 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a new cluster agent token' do
+RSpec.describe 'Create a new cluster agent token', feature_category: :kubernetes_management do
include GraphqlHelpers
let_it_be(:cluster_agent) { create(:cluster_agent) }
diff --git a/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb
index c2ef2362d66..66e6c5cc629 100644
--- a/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a new cluster agent' do
+RSpec.describe 'Create a new cluster agent', feature_category: :kubernetes_management do
include GraphqlHelpers
let(:project) { create(:project, :public, :repository) }
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 4891e64aab8..27a566dfb8c 100644
--- a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Delete a cluster agent' do
+RSpec.describe 'Delete a cluster agent', feature_category: :kubernetes_management do
include GraphqlHelpers
let(:cluster_agent) { create(:cluster_agent) }
diff --git a/spec/requests/api/graphql/mutations/commits/create_spec.rb b/spec/requests/api/graphql/mutations/commits/create_spec.rb
index 619cba99c4e..e298d8284c6 100644
--- a/spec/requests/api/graphql/mutations/commits/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/commits/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creation of a new commit' do
+RSpec.describe 'Creation of a new commit', feature_category: :source_code_management do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
index ca7c1b2ce5f..97e2a5d0bf7 100644
--- a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Updating the container expiration policy' do
+RSpec.describe 'Updating the container expiration policy', feature_category: :container_registry do
include GraphqlHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
index 5a27d39ecbc..8b76c19cda6 100644
--- a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Destroying a container repository' do
+RSpec.describe 'Destroying a container repository', feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
@@ -80,25 +80,6 @@ RSpec.describe 'Destroying a container repository' do
it_behaves_like params[:shared_examples_name]
end
-
- context 'with container_registry_delete_repository_with_cron_worker disabled' do
- before do
- project.add_maintainer(user)
- stub_feature_flags(container_registry_delete_repository_with_cron_worker: false)
- end
-
- it 'enqueues a removal job' do
- expect(::Packages::CreateEventService)
- .to receive(:new).with(nil, user, event_name: :delete_repository, scope: :container).and_call_original
- expect(DeleteContainerRepositoryWorker)
- .to receive(:perform_async).with(user.id, container_repository.id)
-
- expect { subject }.to change { ::Packages::Event.count }.by(1)
-
- expect(container_repository_mutation_response).to match_schema('graphql/container_repository')
- expect(container_repository_mutation_response['status']).to eq('DELETE_SCHEDULED')
- end
- end
end
context 'with invalid id' do
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 ef00f45ef18..9e07a831076 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Destroying a container repository tags' do
+RSpec.describe 'Destroying a container repository tags', feature_category: :container_registry do
include_context 'container repository delete tags service shared context'
using RSpec::Parameterized::TableSyntax
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
index 66facdebe78..ea2ce8a13e2 100644
--- a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creation of a new Custom Emoji' do
+RSpec.describe 'Creation of a new Custom Emoji', feature_category: :not_owned do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
index 7d25206e617..ad7a043909a 100644
--- a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Deletion of custom emoji' do
+RSpec.describe 'Deletion of custom emoji', feature_category: :not_owned do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb b/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb
index 9eb13e534ac..5d5696d3f66 100644
--- a/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Updating the dependency proxy group settings' do
+RSpec.describe 'Updating the dependency proxy group settings', feature_category: :dependency_proxy do
include GraphqlHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb b/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb
index 31ba7ecdf0e..66ee17f356c 100644
--- a/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Updating the dependency proxy image ttl policy' do
+RSpec.describe 'Updating the dependency proxy image ttl policy', feature_category: :dependency_proxy do
include GraphqlHelpers
using RSpec::Parameterized::TableSyntax
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 e2ab08b301b..7ea32ae6d19 100644
--- a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "deleting designs" do
+RSpec.describe "deleting designs", feature_category: :design_management do
include GraphqlHelpers
include DesignManagementTestHelpers
diff --git a/spec/requests/api/graphql/mutations/design_management/move_spec.rb b/spec/requests/api/graphql/mutations/design_management/move_spec.rb
index dd121ec733e..27b5259c56b 100644
--- a/spec/requests/api/graphql/mutations/design_management/move_spec.rb
+++ b/spec/requests/api/graphql/mutations/design_management/move_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "spec_helper"
-RSpec.describe "moving designs" do
+RSpec.describe "moving designs", feature_category: :design_management do
include GraphqlHelpers
include DesignManagementTestHelpers
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 d3e6c689a59..9b42b32c150 100644
--- a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb
+++ b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "spec_helper"
-RSpec.describe "uploading designs" do
+RSpec.describe "uploading designs", feature_category: :design_management do
include GraphqlHelpers
include DesignManagementTestHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb
index 632a934cd95..16d3bbb6518 100644
--- a/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb
+++ b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Toggling the resolve status of a discussion' do
+RSpec.describe 'Toggling the resolve status of a discussion', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project, :public, :repository) }
diff --git a/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb b/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb
index 3771ae0746e..0e9317a4879 100644
--- a/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Update Environment Canary Ingress', :clean_gitlab_redis_cache do
+RSpec.describe 'Update Environment Canary Ingress', :clean_gitlab_redis_cache, feature_category: :deployment_management do
include GraphqlHelpers
include KubernetesHelpers
diff --git a/spec/requests/api/graphql/mutations/groups/update_spec.rb b/spec/requests/api/graphql/mutations/groups/update_spec.rb
index b9dfb8e37ab..ea3d42a4463 100644
--- a/spec/requests/api/graphql/mutations/groups/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/groups/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'GroupUpdate' do
+RSpec.describe 'GroupUpdate', feature_category: :subgroups do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb
index fc3b666dd3d..49cee4f6801 100644
--- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creating an incident timeline event' do
+RSpec.describe 'Creating an incident timeline event', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb
index 85208869ad9..6e1a7b36736 100644
--- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Removing an incident timeline event' do
+RSpec.describe 'Removing an incident timeline event', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
index 62eeecb3fb7..ca9557b3183 100644
--- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Promote an incident timeline event from a comment' do
+RSpec.describe 'Promote an incident timeline event from a comment', feature_category: :incident_management do
include GraphqlHelpers
include NotesHelper
diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb
index 542d51b990f..163c689e399 100644
--- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb
@@ -2,24 +2,36 @@
require 'spec_helper'
-RSpec.describe 'Updating an incident timeline event' do
+RSpec.describe 'Updating an incident timeline event', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:incident) { create(:incident, project: project) }
+ let_it_be(:tag1) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 1') }
+ let_it_be(:tag2) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 2') }
let_it_be_with_reload(:timeline_event) do
create(:incident_management_timeline_event, incident: incident, project: project)
end
+ # Pre-attach a tag to the event
+ let_it_be(:tag_link1) do
+ create(:incident_management_timeline_event_tag_link,
+ timeline_event: timeline_event,
+ timeline_event_tag: tag1
+ )
+ end
+
let(:occurred_at) { 1.minute.ago.iso8601 }
let(:note) { 'Updated note' }
+ let(:tag_names) { [] }
let(:variables) do
{
id: timeline_event.to_global_id.to_s,
note: note,
- occurred_at: occurred_at
+ occurred_at: occurred_at,
+ timeline_event_tag_names: tag_names
}
end
@@ -33,6 +45,7 @@ RSpec.describe 'Updating an incident timeline event' do
author { id username }
updatedByUser { id username }
incident { id title }
+ timelineEventTags { nodes { name } }
note
noteHtml
occurredAt
@@ -71,6 +84,9 @@ RSpec.describe 'Updating an incident timeline event' do
'id' => incident.to_global_id.to_s,
'title' => incident.title
},
+ 'timelineEventTags' => {
+ 'nodes' => []
+ },
'note' => note,
'noteHtml' => timeline_event.note_html,
'occurredAt' => occurred_at,
@@ -85,4 +101,27 @@ RSpec.describe 'Updating an incident timeline event' do
it_behaves_like 'timeline event mutation responds with validation error',
error_message: 'Timeline text is too long (maximum is 280 characters)'
end
+
+ context 'when timeline event tag names are passed' do
+ context 'when tags exist' do
+ let(:tag_names) { [tag2.name] }
+
+ it 'removes tag1 and adds tag2' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ timeline_event_response = mutation_response['timelineEvent']
+ tag_names = timeline_event_response['timelineEventTags']['nodes']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(tag_names).to contain_exactly({ "name" => tag2.name })
+ end
+ end
+
+ context 'when tags do not exist' do
+ let(:tag_names) { ['some other tag'] }
+
+ it_behaves_like 'timeline event mutation responds with validation error',
+ error_message: "Following tags don't exist: [\"some other tag\"]"
+ end
+ end
end
diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb
index 7476499d9da..b37a5331421 100644
--- a/spec/requests/api/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creating a timeline event tag' do
+RSpec.describe 'Creating a timeline event tag', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb
index a489b7424e8..d2d2f0014d6 100644
--- a/spec/requests/api/graphql/mutations/issues/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create an issue' do
+RSpec.describe 'Create an issue', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb b/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb
new file mode 100644
index 00000000000..85e21952f47
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/issues/link_alerts_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Link alerts to an incident', feature_category: :incident_management do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:linked_alert) { create(:alert_management_alert, project: project) }
+ let_it_be(:alert1) { create(:alert_management_alert, project: project) }
+ let_it_be(:alert2) { create(:alert_management_alert, project: project) }
+ let_it_be(:incident) { create(:incident, project: project, alert_management_alerts: [linked_alert]) }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: incident.iid.to_s,
+ 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
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:issue_link_alerts)
+ end
+
+ context 'when the user is not allowed to update the incident' do
+ it 'returns an error' do
+ error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors).to include(a_hash_including('message' => error))
+ end
+ end
+
+ context 'when the user is allowed to update the incident' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'links alerts to the incident' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expected_response = [linked_alert, alert1, alert2].map { |a| { 'iid' => a.iid.to_s } }
+ expect(mutation_response.dig('issue', 'alertManagementAlerts', 'nodes')).to match_array(expected_response)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/issues/move_spec.rb b/spec/requests/api/graphql/mutations/issues/move_spec.rb
index 20ed16879f6..7d9579067b6 100644
--- a/spec/requests/api/graphql/mutations/issues/move_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/move_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Moving an issue' do
+RSpec.describe 'Moving an issue', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
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 12ab504da14..c5e6901d8f8 100644
--- a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting an issue as confidential' do
+RSpec.describe 'Setting an issue as confidential', feature_category: :team_planning do
include GraphqlHelpers
let(:current_user) { create(:user) }
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 395a490bfc3..9fce5f8497f 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting issues crm contacts' do
+RSpec.describe 'Setting issues crm contacts', feature_category: :service_desk do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
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 8e223b6fdaf..1a5a64e4196 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting Due Date of an issue' do
+RSpec.describe 'Setting Due Date of an issue', feature_category: :team_planning do
include GraphqlHelpers
let(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb
index a81364d37b2..8fc3ad4236d 100644
--- a/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting the escalation status of an incident' do
+RSpec.describe 'Setting the escalation status of an incident', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
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 435ed0f9eb2..a8025894b1e 100644
--- a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting an issue as locked' do
+RSpec.describe 'Setting an issue as locked', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
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 cd9d695bd2c..77262c7f64f 100644
--- a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting severity level of an incident' do
+RSpec.describe 'Setting severity level of an incident', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/issues/set_subscription_spec.rb b/spec/requests/api/graphql/mutations/issues/set_subscription_spec.rb
index 1edc1e0553b..6c8e5b1d15d 100644
--- a/spec/requests/api/graphql/mutations/issues/set_subscription_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_subscription_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting subscribed status of an issue' do
+RSpec.describe 'Setting subscribed status of an issue', feature_category: :team_planning do
include GraphqlHelpers
it_behaves_like 'a subscribable resource api' do
diff --git a/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb b/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb
new file mode 100644
index 00000000000..7f6f968b1dd
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/issues/unlink_alerts_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Unlink alert from an incident', feature_category: :incident_management do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:another_project) { create(:project) }
+ let_it_be(:internal_alert) { create(:alert_management_alert, project: project) }
+ let_it_be(:external_alert) { create(:alert_management_alert, project: another_project) }
+ let_it_be(:incident) do
+ create(:incident, project: project, alert_management_alerts: [internal_alert, external_alert])
+ end
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: incident.iid.to_s,
+ 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
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:issue_unlink_alert)
+ end
+
+ context 'when the user is not allowed to update the incident' do
+ let(:alert_to_unlink) { internal_alert }
+
+ it 'returns an error' do
+ error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors).to include(a_hash_including('message' => error))
+ end
+ end
+
+ context 'when the user is allowed to update the incident' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ shared_examples 'unlinking' do
+ it 'unlinks the alert from the incident', :aggregate_failures do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expected_response = visible_remainded_alerts.map { |a| { 'id' => a.to_global_id.to_s } }
+ expect(mutation_response.dig('issue', 'alertManagementAlerts', 'nodes')).to match_array(expected_response)
+
+ expect(incident.reload.alert_management_alerts).to match_array(actual_remainded_alerts)
+ end
+ end
+
+ context 'when the alert is internal' do
+ let(:alert_to_unlink) { internal_alert }
+ let(:actual_remainded_alerts) { [external_alert] }
+ let(:visible_remainded_alerts) { [] } # The user cannot fetch external alerts without reading permissions
+
+ it_behaves_like 'unlinking'
+ end
+
+ context 'when the alert is external' do
+ let(:alert_to_unlink) { external_alert }
+ let(:actual_remainded_alerts) { [internal_alert] }
+ let(:visible_remainded_alerts) { [internal_alert] }
+
+ it_behaves_like 'unlinking'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/issues/update_spec.rb b/spec/requests/api/graphql/mutations/issues/update_spec.rb
index f38deb426b1..e51c057c182 100644
--- a/spec/requests/api/graphql/mutations/issues/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Update of an existing issue' do
+RSpec.describe 'Update of an existing issue', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb b/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb
index b438e1ba881..ab15aa97680 100644
--- a/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb
+++ b/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Importing Jira Users' do
+RSpec.describe 'Importing Jira Users', feature_category: :integrations do
include JiraIntegrationHelpers
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb
index 1508ba31e37..a864bc88afc 100644
--- a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb
+++ b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Starting a Jira Import' do
+RSpec.describe 'Starting a Jira Import', feature_category: :integrations do
include JiraIntegrationHelpers
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/mutations/labels/create_spec.rb b/spec/requests/api/graphql/mutations/labels/create_spec.rb
index d19411f6c1d..607e20af977 100644
--- a/spec/requests/api/graphql/mutations/labels/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/labels/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::Labels::Create do
+RSpec.describe Mutations::Labels::Create, feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb
index 3a4508489a1..c954fd50cc4 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creation of a new merge request' do
+RSpec.describe 'Creation of a new merge request', feature_category: :code_review do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
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 2e4f35cbcde..c41161eff2b 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting assignees of a merge request' do
+RSpec.describe 'Setting assignees of a merge request', feature_category: :code_review do
include GraphqlHelpers
let(:current_user) { create(:user) }
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 8cec5867aca..364d13291db 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting assignees of a merge request', :assume_throttled do
+RSpec.describe 'Setting assignees of a merge request', :assume_throttled, feature_category: :code_review do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
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 bea2365eaa6..b48a94fbeb9 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting Draft status of a merge request' do
+RSpec.describe 'Setting Draft status of a merge request', feature_category: :code_review do
include GraphqlHelpers
let(:current_user) { create(:user) }
@@ -64,7 +64,7 @@ RSpec.describe 'Setting Draft status of a merge request' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['mergeRequest']['title']).not_to start_with(/draft\:/)
+ expect(mutation_response['mergeRequest']['title']).not_to start_with(/draft:/)
end
it 'unmarks the merge request as `Draft`' do
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 a1a35bc1dcc..d88982c508c 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting locked status of a merge request' do
+RSpec.describe 'Setting locked status of a merge request', feature_category: :code_review do
include GraphqlHelpers
let(:current_user) { create(:user) }
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 d7e2602bd0a..a0f0e45d1fc 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting milestone of a merge request' do
+RSpec.describe 'Setting milestone of a merge request', feature_category: :code_review do
include GraphqlHelpers
let(:current_user) { create(:user) }
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 be786256ef2..a5be2a95c8b 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting reviewers of a merge request', :assume_throttled do
+RSpec.describe 'Setting reviewers of a merge request', :assume_throttled, feature_category: :code_review do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb
index d90faa605c0..daf1f529847 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Setting subscribed status of a merge request' do
+RSpec.describe 'Setting subscribed status of a merge request', feature_category: :code_review do
include GraphqlHelpers
it_behaves_like 'a subscribable resource api' do
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
index 9ef443af76a..bce57b47aab 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do
+RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create, feature_category: :metrics do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
index b956734068c..f505dc25dc0 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do
+RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_category: :metrics do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
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 567d8799d93..f4f4f34fe29 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Updating the package settings' do
+RSpec.describe 'Updating the package settings', feature_category: :package_registry do
include GraphqlHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb
index a432fb17a70..3cf78230a4c 100644
--- a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Adding a DiffNote' do
+RSpec.describe 'Adding a DiffNote', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb
index 8f2438cb741..0ce239d9ca5 100644
--- a/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Adding an image DiffNote' do
+RSpec.describe 'Adding an image DiffNote', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
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 9c3842db31a..00e25909746 100644
--- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Adding a Note' do
+RSpec.describe 'Adding a Note', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
@@ -102,6 +102,35 @@ RSpec.describe 'Adding a Note' do
it_behaves_like 'a Note mutation with confidential notes'
end
+
+ context 'as work item' do
+ let(:noteable) { create(:work_item, :issue, project: project) }
+
+ context 'when using internal param' do
+ let(:variables_extra) { { internal: true } }
+
+ it_behaves_like 'a Note mutation with confidential notes'
+ end
+
+ context 'when using deprecated confidential param' do
+ let(:variables_extra) { { confidential: true } }
+
+ it_behaves_like 'a Note mutation with confidential notes'
+ end
+
+ context 'without notes widget' do
+ let(:variables_extra) { {} }
+
+ before do
+ stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
+ stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+ end
+
+ it_behaves_like 'a Note mutation that does not create a Note'
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ end
+ end
end
context 'when body only contains quick actions' do
diff --git a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
index 49f09fadfea..eb45e2aa033 100644
--- a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
@@ -2,17 +2,14 @@
require 'spec_helper'
-RSpec.describe 'Destroying a Note' do
+RSpec.describe 'Destroying a Note', feature_category: :team_planning do
include GraphqlHelpers
- let!(:note) { create(:note) }
- let(:mutation) do
- variables = {
- id: GitlabSchema.id_from_object(note).to_s
- }
-
- graphql_mutation(:destroy_note, variables)
- end
+ let(:noteable) { create(:work_item, :issue) }
+ let!(:note) { create(:note, noteable: noteable, project: noteable.project) }
+ let(:global_note_id) { GitlabSchema.id_from_object(note).to_s }
+ let(:variables) { { id: global_note_id } }
+ let(:mutation) { graphql_mutation(:destroy_note, variables) }
def mutation_response
graphql_mutation_response(:destroy_note)
@@ -47,5 +44,31 @@ RSpec.describe 'Destroying a Note' do
expect(mutation_response).to have_key('note')
expect(mutation_response['note']).to be_nil
end
+
+ context 'when note is system' do
+ let!(:note) { create(:note, :system) }
+
+ it 'does not destroy system note' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.not_to change { Note.count }
+ end
+ end
+
+ context 'without notes widget' do
+ before do
+ stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
+ stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+ end
+
+ it 'does not update the Note' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to not_change { Note.count }
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb
index c4674155aa0..e9cd27a1e20 100644
--- a/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Repositioning an ImageDiffNote' do
+RSpec.describe 'Repositioning an ImageDiffNote', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:noteable) { create(:merge_request) }
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 cfd0b34b815..a5cd3c8b019 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Updating an image DiffNote' do
+RSpec.describe 'Updating an image DiffNote', feature_category: :team_planning do
include GraphqlHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
index bae5c58abff..dff8a87314b 100644
--- a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Updating a Note' do
+RSpec.describe 'Updating a Note', feature_category: :team_planning do
include GraphqlHelpers
let!(:note) { create(:note, note: original_body) }
@@ -36,49 +36,32 @@ RSpec.describe 'Updating a Note' do
it_behaves_like 'a Note mutation when the given resource id is not for a Note'
- it 'updates the Note' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(note.reload.note).to eq(updated_body)
- end
-
- it 'returns the updated Note' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(mutation_response['note']['body']).to eq(updated_body)
- end
+ it_behaves_like 'a Note mutation updates a note successfully'
+ it_behaves_like 'a Note mutation update with errors'
+ it_behaves_like 'a Note mutation update only with quick actions'
- context 'when there are ActiveRecord validation errors' do
- let(:params) { { body: '', confidential: true } }
+ context 'for work item' do
+ let(:noteable) { create(:work_item, :issue) }
+ let(:note) { create(:note, noteable: noteable, project: noteable.project, note: original_body) }
- it_behaves_like 'a mutation that returns errors in the response',
- errors: ["Note can't be blank", 'Confidential can not be changed for existing notes']
+ it_behaves_like 'a Note mutation updates a note successfully'
+ it_behaves_like 'a Note mutation update with errors'
+ it_behaves_like 'a Note mutation update only with quick actions'
- it 'does not update the Note' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(note.reload.note).to eq(original_body)
- expect(note.confidential).to be_falsey
- end
-
- it 'returns the original Note' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(mutation_response['note']['body']).to eq(original_body)
- expect(mutation_response['note']['confidential']).to be_falsey
- end
- end
+ context 'without notes widget' do
+ before do
+ stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
+ stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+ end
- context 'when body only contains quick actions' do
- let(:updated_body) { '/close' }
+ it 'does not update the Note' do
+ post_graphql_mutation(mutation, current_user: current_user)
- it 'returns a nil note and empty errors' do
- post_graphql_mutation(mutation, current_user: current_user)
+ expect(note.reload.note).to eq(original_body)
+ end
- expect(mutation_response).to include(
- 'errors' => [],
- 'note' => nil
- )
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
end
end
diff --git a/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb b/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb
index 1fe01af4f1c..d0980a2b43d 100644
--- a/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/packages/bulk_destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Destroying multiple packages' do
+RSpec.describe 'Destroying multiple packages', feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
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 7e00f3ca53a..2540e06be9a 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Updating the packages cleanup policy' do
+RSpec.describe 'Updating the packages cleanup policy', feature_category: :package_registry do
include GraphqlHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/requests/api/graphql/mutations/packages/destroy_file_spec.rb b/spec/requests/api/graphql/mutations/packages/destroy_file_spec.rb
index cd25aba9e00..a4b7001fdd5 100644
--- a/spec/requests/api/graphql/mutations/packages/destroy_file_spec.rb
+++ b/spec/requests/api/graphql/mutations/packages/destroy_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Destroying a package file' do
+RSpec.describe 'Destroying a package file', feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/mutations/packages/destroy_files_spec.rb b/spec/requests/api/graphql/mutations/packages/destroy_files_spec.rb
index 002cd634ebd..cdd05d80fc2 100644
--- a/spec/requests/api/graphql/mutations/packages/destroy_files_spec.rb
+++ b/spec/requests/api/graphql/mutations/packages/destroy_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Destroying multiple package files' do
+RSpec.describe 'Destroying multiple package files', feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/mutations/packages/destroy_spec.rb b/spec/requests/api/graphql/mutations/packages/destroy_spec.rb
index 2340a6a36d8..86167e7116f 100644
--- a/spec/requests/api/graphql/mutations/packages/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/packages/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Destroying a package' do
+RSpec.describe 'Destroying a package', feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb
index c7a4cb1ebce..418a0e47a36 100644
--- a/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creation of a new release asset link' do
+RSpec.describe 'Creation of a new release asset link', feature_category: :release_orchestration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :private, :repository) }
diff --git a/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb
index 57489c82ec2..b6d2c3f691d 100644
--- a/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Deletes a release asset link' do
+RSpec.describe 'Deletes a release asset link', feature_category: :release_orchestration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :private, :repository) }
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 92b558d4be3..61395cc4042 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Updating an existing release asset link' do
+RSpec.describe 'Updating an existing release asset link', feature_category: :release_orchestration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :private, :repository) }
diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb
index 2541072b766..295b8c0e97e 100644
--- a/spec/requests/api/graphql/mutations/releases/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creation of a new release' do
+RSpec.describe 'Creation of a new release', feature_category: :release_orchestration do
include GraphqlHelpers
include Presentable
diff --git a/spec/requests/api/graphql/mutations/releases/delete_spec.rb b/spec/requests/api/graphql/mutations/releases/delete_spec.rb
index eb4f0b594ea..bb398787cc6 100644
--- a/spec/requests/api/graphql/mutations/releases/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/releases/delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Deleting a release' do
+RSpec.describe 'Deleting a release', feature_category: :release_orchestration do
include GraphqlHelpers
include Presentable
diff --git a/spec/requests/api/graphql/mutations/releases/update_spec.rb b/spec/requests/api/graphql/mutations/releases/update_spec.rb
index 240db764f40..2b88576a70e 100644
--- a/spec/requests/api/graphql/mutations/releases/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/releases/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Updating an existing release' do
+RSpec.describe 'Updating an existing release', feature_category: :release_orchestration do
include GraphqlHelpers
include Presentable
diff --git a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
index 0c034f38dc8..cdd25f8f6ec 100644
--- a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
+++ b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'ConfigureSastIac' do
+RSpec.describe 'ConfigureSastIac', feature_category: :static_application_security_testing do
include GraphqlHelpers
let_it_be(:project) { create(:project, :test_repo) }
diff --git a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb
index 8fa6e44b208..370abf2fe00 100644
--- a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb
+++ b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_secret_detection_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'ConfigureSecretDetection' do
+RSpec.describe 'ConfigureSecretDetection', feature_category: :secret_detection do
include GraphqlHelpers
let_it_be(:project) { create(:project, :test_repo) }
diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
index 264fa5732c3..0b1af2bf628 100644
--- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creating a Snippet' do
+RSpec.describe 'Creating a Snippet', feature_category: :source_code_management do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
index 1be8ce142ac..09e884d9412 100644
--- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Destroying a Snippet' do
+RSpec.describe 'Destroying a Snippet', feature_category: :source_code_management do
include GraphqlHelpers
let(:current_user) { snippet.author }
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 77fd6cddc09..9a8c027da8a 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Mark snippet as spam' do
+RSpec.describe 'Mark snippet as spam', feature_category: :source_code_management do
include GraphqlHelpers
include AfterNextHelpers
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index 1a5d3620f22..fa087e6773c 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Updating a Snippet' do
+RSpec.describe 'Updating a Snippet', feature_category: :source_code_management do
include GraphqlHelpers
include SessionHelpers
@@ -192,11 +192,18 @@ RSpec.describe 'Updating a Snippet' do
stub_session('warden.user.user.key' => [[current_user.id], current_user.authenticatable_salt])
end
- it_behaves_like 'Snowplow event tracking' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:user) { current_user }
+ let(:property) { 'g_edit_by_snippet_ide' }
let(:namespace) { project.namespace }
- let(:category) { 'ide_edit' }
- let(:action) { 'g_edit_by_snippet_ide' }
+ let(:category) { 'Gitlab::UsageDataCounters::EditorUniqueCounter' }
+ let(:action) { 'ide_edit' }
+ let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_ide_edit' }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name).to_context]
+ end
+
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
end
end
diff --git a/spec/requests/api/graphql/mutations/timelogs/create_spec.rb b/spec/requests/api/graphql/mutations/timelogs/create_spec.rb
index eea04b89783..42249818a92 100644
--- a/spec/requests/api/graphql/mutations/timelogs/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/timelogs/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a timelog' do
+RSpec.describe 'Create a timelog', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:author) { create(:user) }
@@ -11,14 +11,6 @@ RSpec.describe 'Create a timelog' do
let(:current_user) { nil }
let(:users_container) { project }
- let(:mutation) do
- graphql_mutation(:timelogCreate, {
- 'time_spent' => time_spent,
- 'spent_at' => '2022-07-08',
- 'summary' => 'Test summary',
- 'issuable_id' => issuable.to_global_id.to_s
- })
- end
let(:mutation_response) { graphql_mutation_response(:timelog_create) }
diff --git a/spec/requests/api/graphql/mutations/timelogs/delete_spec.rb b/spec/requests/api/graphql/mutations/timelogs/delete_spec.rb
index d304bfbdf00..d04b4d193e6 100644
--- a/spec/requests/api/graphql/mutations/timelogs/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/timelogs/delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Delete a timelog' do
+RSpec.describe 'Delete a timelog', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/requests/api/graphql/mutations/todos/create_spec.rb b/spec/requests/api/graphql/mutations/todos/create_spec.rb
index aca00519682..5d7ecb3c927 100644
--- a/spec/requests/api/graphql/mutations/todos/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a todo' do
+RSpec.describe 'Create a todo', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
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 dc20fde8e3c..c611c6ee2a1 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Marking all todos done' do
+RSpec.describe 'Marking all todos done', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
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 7f5ea71c760..60700d8024c 100644
--- a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Marking todos done' do
+RSpec.describe 'Marking todos done', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
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 4316bd060c1..9daa243cf8e 100644
--- a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Restoring many Todos' do
+RSpec.describe 'Restoring many Todos', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/mutations/todos/restore_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_spec.rb
index d995191c97e..868298763ec 100644
--- a/spec/requests/api/graphql/mutations/todos/restore_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/restore_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Restoring Todos' do
+RSpec.describe 'Restoring Todos', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/mutations/uploads/delete_spec.rb b/spec/requests/api/graphql/mutations/uploads/delete_spec.rb
index 2d1b33cc086..08dbbe23b6b 100644
--- a/spec/requests/api/graphql/mutations/uploads/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/uploads/delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Delete an upload' do
+RSpec.describe 'Delete an upload', feature_category: :navigation do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb b/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb
index 28a46583d2a..eb35d310760 100644
--- a/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a user callout' do
+RSpec.describe 'Create a user callout', feature_category: :navigation do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb
index e1c7fd9d60d..31d17401b9e 100644
--- a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::UserPreferences::Update do
+RSpec.describe Mutations::UserPreferences::Update, feature_category: :users do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
index c6a980b5cef..97bf060356a 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Create a work item from a task in a work item's description" do
+RSpec.describe "Create a work item from a task in a work item's description", feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
index be3917316c3..16f78b67b5c 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a work item' do
+RSpec.describe 'Create a work item', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
@@ -123,7 +123,7 @@ RSpec.describe 'Create a work item' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors'])
- .to contain_exactly(/cannot be added: only Issue and Incident can be parent of Task./)
+ .to contain_exactly(/cannot be added: is not allowed to add this type of parent/)
expect(mutation_response['workItem']).to be_nil
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/delete_spec.rb b/spec/requests/api/graphql/mutations/work_items/delete_spec.rb
index 0a84225a7ab..e25ff5613e4 100644
--- a/spec/requests/api/graphql/mutations/work_items/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Delete a work item' do
+RSpec.describe 'Delete a work item', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb
index c44939c8d54..b1828de046f 100644
--- a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Delete a task in a work item's description" do
+RSpec.describe "Delete a task in a work item's description", feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index 96736457f26..14cb18d04b8 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Update a work item' do
+RSpec.describe 'Update a work item', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
@@ -339,7 +339,7 @@ RSpec.describe 'Update a work item' do
let_it_be(:invalid_parent) { create(:work_item, :task, project: project) }
context 'when parent work item type is invalid' do
- let(:error) { "#{work_item.to_reference} cannot be added: only Issue and Incident can be parent of Task." }
+ let(:error) { "#{work_item.to_reference} cannot be added: is not allowed to add this type of parent" }
let(:input) do
{ 'hierarchyWidget' => { 'parentId' => invalid_parent.to_global_id.to_s }, 'title' => 'new title' }
end
@@ -450,7 +450,7 @@ RSpec.describe 'Update a work item' do
let(:input) { { 'hierarchyWidget' => { 'childrenIds' => children_ids } } }
let(:error) do
- "#{invalid_child.to_reference} cannot be added: only Task can be assigned as a child in hierarchy."
+ "#{invalid_child.to_reference} cannot be added: is not allowed to add this type of parent"
end
context 'when child work item type is invalid' do
@@ -632,7 +632,7 @@ RSpec.describe 'Update a work item' do
end
context 'when unsupported widget input is sent' do
- let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') }
+ let_it_be(:test_case) { create(:work_item_type, :default, :test_case) }
let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }
let(:input) do
@@ -642,7 +642,7 @@ RSpec.describe 'Update a work item' do
end
it_behaves_like 'a mutation that returns top-level errors',
- errors: ["Following widget keys are not supported by some_test_case_name type: [:hierarchy_widget]"]
+ errors: ["Following widget keys are not supported by Test Case type: [:hierarchy_widget]"]
end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
index 55285be5a5d..999c685ac6a 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Update a work item task' do
+RSpec.describe 'Update a work item task', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/namespace/package_settings_spec.rb b/spec/requests/api/graphql/namespace/package_settings_spec.rb
index 42fd07dbdc7..bd441032626 100644
--- a/spec/requests/api/graphql/namespace/package_settings_spec.rb
+++ b/spec/requests/api/graphql/namespace/package_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting namespace package settings in a namespace' do
+RSpec.describe 'getting namespace package settings in a namespace', feature_category: :package_registry do
include GraphqlHelpers
let_it_be(:package_settings) { create(:namespace_package_setting) }
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
index d5410f1a7cb..4e12da3e3ab 100644
--- a/spec/requests/api/graphql/namespace/projects_spec.rb
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting projects' do
+RSpec.describe 'getting projects', feature_category: :projects do
include GraphqlHelpers
let(:group) { create(:group) }
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 8d8a0baae36..cee698d6dc5 100644
--- a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
+++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'rendering namespace statistics' do
+RSpec.describe 'rendering namespace statistics', feature_category: :metrics do
include GraphqlHelpers
let(:namespace) { user.namespace }
diff --git a/spec/requests/api/graphql/namespace_query_spec.rb b/spec/requests/api/graphql/namespace_query_spec.rb
index e17469901c6..d12a3875ebf 100644
--- a/spec/requests/api/graphql/namespace_query_spec.rb
+++ b/spec/requests/api/graphql/namespace_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query' do
+RSpec.describe 'Query', feature_category: :subgroups do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/packages/composer_spec.rb b/spec/requests/api/graphql/packages/composer_spec.rb
index 89c01d44771..dd61582b055 100644
--- a/spec/requests/api/graphql/packages/composer_spec.rb
+++ b/spec/requests/api/graphql/packages/composer_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'package details' do
+RSpec.describe 'package details', feature_category: :package_registry do
include GraphqlHelpers
include_context 'package details setup'
diff --git a/spec/requests/api/graphql/packages/conan_spec.rb b/spec/requests/api/graphql/packages/conan_spec.rb
index 7ad85edecef..b8226c482ac 100644
--- a/spec/requests/api/graphql/packages/conan_spec.rb
+++ b/spec/requests/api/graphql/packages/conan_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'conan package details' do
+RSpec.describe 'conan package details', feature_category: :package_registry do
include GraphqlHelpers
include_context 'package details setup'
diff --git a/spec/requests/api/graphql/packages/helm_spec.rb b/spec/requests/api/graphql/packages/helm_spec.rb
index 79a589e2dc2..65b3f80d6df 100644
--- a/spec/requests/api/graphql/packages/helm_spec.rb
+++ b/spec/requests/api/graphql/packages/helm_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'helm package details' do
+RSpec.describe 'helm package details', feature_category: :package_registry do
include GraphqlHelpers
include_context 'package details setup'
diff --git a/spec/requests/api/graphql/packages/maven_spec.rb b/spec/requests/api/graphql/packages/maven_spec.rb
index b7f39efcf73..26c45ada4a1 100644
--- a/spec/requests/api/graphql/packages/maven_spec.rb
+++ b/spec/requests/api/graphql/packages/maven_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'maven package details' do
+RSpec.describe 'maven package details', feature_category: :package_registry do
include GraphqlHelpers
include_context 'package details setup'
diff --git a/spec/requests/api/graphql/packages/nuget_spec.rb b/spec/requests/api/graphql/packages/nuget_spec.rb
index 7de132d1574..1c3af46909e 100644
--- a/spec/requests/api/graphql/packages/nuget_spec.rb
+++ b/spec/requests/api/graphql/packages/nuget_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'nuget package details' do
+RSpec.describe 'nuget package details', feature_category: :package_registry do
include GraphqlHelpers
include_context 'package details setup'
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index 02a3206f587..42927634119 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'package details' do
+RSpec.describe 'package details', feature_category: :package_registry do
include GraphqlHelpers
let_it_be_with_reload(:group) { create(:group) }
@@ -226,5 +226,16 @@ RSpec.describe 'package details' do
end
end
end
+
+ context 'with package that has no default status' do
+ before do
+ composer_package.update!(status: :error)
+ subject
+ end
+
+ it "does not return package's details" do
+ expect(package_details).to be_nil
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/packages/pypi_spec.rb b/spec/requests/api/graphql/packages/pypi_spec.rb
index c0e589f3597..4441f04dabb 100644
--- a/spec/requests/api/graphql/packages/pypi_spec.rb
+++ b/spec/requests/api/graphql/packages/pypi_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'pypi package details' do
+RSpec.describe 'pypi package details', feature_category: :package_registry do
include GraphqlHelpers
include_context 'package details setup'
diff --git a/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb
index a59402208ec..c4843c3cf97 100644
--- a/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting Alert Management Alert Assignees' do
+RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :projects do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb
index 29896c16f5b..3c9ec4fb60b 100644
--- a/spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting Alert Management Alert Issue' do
+RSpec.describe 'getting Alert Management Alert Issue', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb
index 352a94cfc1d..b430fdeb18f 100644
--- a/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting Alert Management Alert Assignees' do
+RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :projects do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
index 72d185144ef..16dd0dfcfcb 100644
--- a/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting Alert Management Alert Notes' do
+RSpec.describe 'getting Alert Management Alert Notes', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb
index ca58079fdfe..ad4361dfa50 100644
--- a/spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting Alert Management Alert Assignees' do
+RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb
index ecd93d169d3..7ce5bf23357 100644
--- a/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting Alert Management Alert counts by status' do
+RSpec.describe 'getting Alert Management Alert counts by status', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
index fe77d9dc86d..304edfbf4e4 100644
--- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting Alert Management Alerts' do
+RSpec.describe 'getting Alert Management Alerts', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' }, 'runbook' => 'runbook' } }
@@ -59,6 +59,7 @@ RSpec.describe 'getting Alert Management Alerts' do
it 'returns the correct properties of the alerts' do
expect(first_alert).to include(
+ 'id' => triggered_alert.to_global_id.to_s,
'iid' => triggered_alert.iid.to_s,
'title' => triggered_alert.title,
'description' => triggered_alert.description,
@@ -80,6 +81,7 @@ RSpec.describe 'getting Alert Management Alerts' do
)
expect(second_alert).to include(
+ 'id' => resolved_alert.to_global_id.to_s,
'iid' => resolved_alert.iid.to_s,
'status' => 'RESOLVED',
'endedAt' => resolved_alert.ended_at.strftime('%Y-%m-%dT%H:%M:%SZ')
diff --git a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
index 773922c1864..e8d19513a4e 100644
--- a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting Alert Management Integrations' do
+RSpec.describe 'getting Alert Management Integrations', feature_category: :integrations do
include ::Gitlab::Routing
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/project/base_service_spec.rb b/spec/requests/api/graphql/project/base_service_spec.rb
index 58d10ade8cf..7b1b95eaf58 100644
--- a/spec/requests/api/graphql/project/base_service_spec.rb
+++ b/spec/requests/api/graphql/project/base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'query Jira service' do
+RSpec.describe 'query Jira service', feature_category: :authentication_and_authorization do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb
index a80f683ea93..d7672cc5116 100644
--- a/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb
+++ b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb
@@ -2,6 +2,6 @@
require 'spec_helper'
-RSpec.describe 'getting merge access levels for a branch protection' do
- include_examples 'perform graphql requests for AccessLevel type objects', :merge
+RSpec.describe 'getting merge access levels for a branch protection', feature_category: :source_code_management do
+ it_behaves_like 'a GraphQL query for access levels', :merge
end
diff --git a/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb
index cfdaf1096c3..65b9bc93a6b 100644
--- a/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb
+++ b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb
@@ -2,6 +2,6 @@
require 'spec_helper'
-RSpec.describe 'getting push access levels for a branch protection' do
- include_examples 'perform graphql requests for AccessLevel type objects', :push
+RSpec.describe 'getting push access levels for a branch protection', feature_category: :source_code_management do
+ it_behaves_like 'a GraphQL query for access levels', :push
end
diff --git a/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb b/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb
index 8a3f546ef95..560819cb989 100644
--- a/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb
+++ b/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting branch protection for a branch rule' do
+RSpec.describe 'getting branch protection for a branch rule', feature_category: :source_code_management do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/project/branch_rules_spec.rb b/spec/requests/api/graphql/project/branch_rules_spec.rb
index ed866305445..7f6a66e2377 100644
--- a/spec/requests/api/graphql/project/branch_rules_spec.rb
+++ b/spec/requests/api/graphql/project/branch_rules_spec.rb
@@ -2,21 +2,11 @@
require 'spec_helper'
-RSpec.describe 'getting list of branch rules for a project' do
+RSpec.describe 'getting list of branch rules for a project', feature_category: :source_code_management do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:branch_name_a) { TestEnv::BRANCH_SHA.each_key.first }
- let_it_be(:branch_name_b) { 'diff-*' }
- let_it_be(:branch_rules) { [branch_rule_a, branch_rule_b] }
- let_it_be(:branch_rule_a) do
- create(:protected_branch, project: project, name: branch_name_a)
- end
-
- let_it_be(:branch_rule_b) do
- create(:protected_branch, project: project, name: branch_name_b)
- end
let(:branch_rules_data) { graphql_data_at('project', 'branchRules', 'edges') }
let(:variables) { { path: project.full_path } }
@@ -61,39 +51,39 @@ RSpec.describe 'getting list of branch rules for a project' do
end
describe 'queries' do
+ include_context 'when user tracking is disabled'
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!) {
+ project(fullPath: $path) {
+ branchRules {
+ nodes {
+ matchingBranchesCount
+ }
+ }
+ }
+ }
+ GQL
+ end
+
before do
- # rubocop:disable RSpec/AnyInstanceOf
- allow_any_instance_of(User).to receive(:update_tracked_fields!)
- allow_any_instance_of(Users::ActivityService).to receive(:execute)
- # rubocop:enable RSpec/AnyInstanceOf
+ create(:protected_branch, project: project)
allow_next_instance_of(Resolvers::ProjectResolver) do |resolver|
allow(resolver).to receive(:resolve)
.with(full_path: project.full_path)
.and_return(project)
end
allow(project.repository).to receive(:branch_names).and_call_original
- allow(project.repository.gitaly_ref_client).to receive(:branch_names).and_call_original
end
- it 'matching_branches_count avoids N+1 queries' do
- query = <<~GQL
- query($path: ID!) {
- project(fullPath: $path) {
- branchRules {
- nodes {
- matchingBranchesCount
- }
- }
- }
- }
- GQL
-
- control = ActiveRecord::QueryRecorder.new do
+ it 'avoids N+1 queries', :use_sql_query_cache, :aggregate_failures do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
post_graphql(query, current_user: current_user, variables: variables)
end
# Verify the response includes the field
- expect_n_matching_branches_count_fields(2)
+ expect_n_matching_branches_count_fields(1)
create(:protected_branch, project: project)
create(:protected_branch, name: '*', project: project)
@@ -102,10 +92,8 @@ RSpec.describe 'getting list of branch rules for a project' do
post_graphql(query, current_user: current_user, variables: variables)
end.not_to exceed_all_query_limit(control)
+ expect_n_matching_branches_count_fields(3)
expect(project.repository).to have_received(:branch_names).at_least(2).times
- expect(project.repository.gitaly_ref_client).to have_received(:branch_names).once
-
- expect_n_matching_branches_count_fields(4)
end
def expect_n_matching_branches_count_fields(count)
@@ -118,21 +106,28 @@ RSpec.describe 'getting list of branch rules for a project' do
end
describe 'response' do
+ let_it_be(:branch_name_a) { TestEnv::BRANCH_SHA.each_key.first }
+ let_it_be(:branch_name_b) { 'diff-*' }
+ let_it_be(:branch_rules) { [branch_rule_a, branch_rule_b] }
+ let_it_be(:branch_rule_a) do
+ create(:protected_branch, project: project, name: branch_name_a, id: 9999)
+ end
+
+ let_it_be(:branch_rule_b) do
+ create(:protected_branch, project: project, name: branch_name_b, id: 10000)
+ end
+
+ # branchRules are returned in reverse order, newest first, sorted by primary_key.
+ let(:branch_rule_b_data) { branch_rules_data.dig(0, 'node') }
+ let(:branch_rule_a_data) { branch_rules_data.dig(1, 'node') }
+
before do
post_graphql(query, current_user: current_user, variables: variables)
end
it_behaves_like 'a working graphql query'
- it 'includes all fields', :aggregate_failures do
- # Responses will be sorted alphabetically. Branch names for this spec
- # come from an external constant so we check which is first
- br_a_idx = branch_name_a < branch_name_b ? 0 : 1
- br_b_idx = 1 - br_a_idx
-
- branch_rule_a_data = branch_rules_data.dig(br_a_idx, 'node')
- branch_rule_b_data = branch_rules_data.dig(br_b_idx, 'node')
-
+ it 'includes all fields', :use_sql_query_cache, :aggregate_failures do
expect(branch_rule_a_data['name']).to eq(branch_name_a)
expect(branch_rule_a_data['isDefault']).to be(true).or be(false)
expect(branch_rule_a_data['branchProtection']).to be_present
diff --git a/spec/requests/api/graphql/project/cluster_agents_spec.rb b/spec/requests/api/graphql/project/cluster_agents_spec.rb
index bb716cf2849..0881eb9cdc3 100644
--- a/spec/requests/api/graphql/project/cluster_agents_spec.rb
+++ b/spec/requests/api/graphql/project/cluster_agents_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project.cluster_agents' do
+RSpec.describe 'Project.cluster_agents', feature_category: :kubernetes_management do
include GraphqlHelpers
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/requests/api/graphql/project/container_expiration_policy_spec.rb b/spec/requests/api/graphql/project/container_expiration_policy_spec.rb
index e3ea9e46353..e484e0618aa 100644
--- a/spec/requests/api/graphql/project/container_expiration_policy_spec.rb
+++ b/spec/requests/api/graphql/project/container_expiration_policy_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting a repository in a project' do
+RSpec.describe 'getting a repository in a project', feature_category: :container_registry do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 01b117a89d8..7ccf8a6f5bf 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting container repositories in a project' do
+RSpec.describe 'getting container repositories in a project', feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/project/deployment_spec.rb b/spec/requests/api/graphql/project/deployment_spec.rb
index e5ef7bcafbf..e3ec33ec131 100644
--- a/spec/requests/api/graphql/project/deployment_spec.rb
+++ b/spec/requests/api/graphql/project/deployment_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Deployment query' do
+RSpec.describe 'Project Deployment query', feature_category: :continuous_delivery do
let_it_be(:project) { create(:project, :private, :repository) }
let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb
index e5b6aebbf2c..618f591affa 100644
--- a/spec/requests/api/graphql/project/environments_spec.rb
+++ b/spec/requests/api/graphql/project/environments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Environments query' do
+RSpec.describe 'Project Environments query', feature_category: :continuous_delivery do
include GraphqlHelpers
let_it_be(:project) { create(:project, :private, :repository) }
@@ -47,6 +47,60 @@ RSpec.describe 'Project Environments query' do
expect(environment_data['environmentType']).to eq(production.environment_type)
end
+ describe 'user permissions' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{production.name}") {
+ userPermissions {
+ updateEnvironment
+ destroyEnvironment
+ stopEnvironment
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns user permissions of the environment', :aggregate_failures do
+ subject
+
+ permission_data = graphql_data.dig('project', 'environment', 'userPermissions')
+ expect(permission_data['updateEnvironment']).to eq(true)
+ expect(permission_data['destroyEnvironment']).to eq(false)
+ expect(permission_data['stopEnvironment']).to eq(true)
+ end
+
+ context 'when fetching user permissions for multiple environments' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environments {
+ nodes {
+ userPermissions {
+ updateEnvironment
+ destroyEnvironment
+ stopEnvironment
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'limits the result', :aggregate_failures do
+ subject
+
+ expect_graphql_errors_to_include('"userPermissions" field can be requested only ' \
+ 'for 1 Environment(s) at a time.')
+ end
+ end
+ end
+
describe 'last deployments of environments' do
::Deployment.statuses.each do |status, _|
let_it_be(:"production_#{status}_deployment") do
@@ -130,4 +184,81 @@ RSpec.describe 'Project Environments query' do
expect(multi).not_to exceed_query_limit(baseline)
end
end
+
+ describe 'nested environments' do
+ let_it_be(:testing1) { create(:environment, name: 'testing/one', project: project) }
+ let_it_be(:testing2) { create(:environment, name: 'testing/two', project: project) }
+
+ context 'with query' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ nestedEnvironments {
+ nodes {
+ name
+ size
+ environment {
+ name
+ path
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'can fetch nested environments' do
+ subject
+
+ nested_envs = graphql_data.dig('project', 'nestedEnvironments', 'nodes')
+ expect(nested_envs.count).to be(3)
+ expect(nested_envs.pluck('name')).to match_array(%w[production staging testing])
+ expect(nested_envs.pluck('size')).to match_array([1, 1, 2])
+ expect(nested_envs[0].dig('environment', 'name')).to eq(production.name)
+ end
+
+ context 'when user is guest' do
+ let(:user) { create(:user).tap { |u| project.add_guest(u) } }
+
+ it 'returns nothing' do
+ subject
+
+ nested_envs = graphql_data.dig('project', 'nestedEnvironments', 'nodes')
+
+ expect(nested_envs).to be_nil
+ end
+ end
+ end
+
+ context 'when using pagination' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ nestedEnvironments(first: 1) {
+ nodes {
+ name
+ }
+ pageInfo {
+ hasPreviousPage
+ startCursor
+ endCursor
+ hasNextPage
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'supports pagination' do
+ subject
+ nested_envs = graphql_data.dig('project', 'nestedEnvironments')
+ expect(nested_envs['nodes'].count).to eq(1)
+ expect(nested_envs.dig('pageInfo', 'hasNextPage')).to be_truthy
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb
index 2fe5fb593fe..e1a8304dce6 100644
--- a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb
+++ b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting a detailed sentry error' do
+RSpec.describe 'getting a detailed sentry error', feature_category: :error_tracking do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
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 3ca0e35882a..2abb1f62ea9 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
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'sentry errors requests' do
+RSpec.describe 'sentry errors requests', feature_category: :error_tracking do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/requests/api/graphql/project/fork_details_spec.rb b/spec/requests/api/graphql/project/fork_details_spec.rb
new file mode 100644
index 00000000000..efd48b00833
--- /dev/null
+++ b/spec/requests/api/graphql/project/fork_details_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting project fork details', feature_category: :source_code_management do
+ include GraphqlHelpers
+ include ProjectForksHelper
+
+ let_it_be(:project) { create(:project, :public, :repository_private, :repository) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
+ let_it_be(:forked_project) { fork_project(project, current_user, repository: true) }
+
+ let(:queried_project) { forked_project }
+
+ let(:query) do
+ graphql_query_for(:project,
+ { full_path: queried_project.full_path }, <<~QUERY
+ forkDetails(ref: "feature"){
+ ahead
+ behind
+ }
+ QUERY
+ )
+ end
+
+ it 'returns fork details' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['forkDetails']).to eq(
+ { 'ahead' => 1, 'behind' => 29 }
+ )
+ end
+
+ context 'when a project is not a fork' do
+ let(:queried_project) { project }
+
+ it 'does not return fork details' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['forkDetails']).to be_nil
+ end
+ end
+
+ context 'when a user cannot read the code' do
+ let_it_be(:current_user) { create(:user) }
+
+ before do
+ forked_project.update!({
+ repository_access_level: 'private',
+ merge_requests_access_level: 'private'
+ })
+ end
+
+ it 'does not return fork details' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['forkDetails']).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/fork_targets_spec.rb b/spec/requests/api/graphql/project/fork_targets_spec.rb
index b21a11ff4dc..f2a3901b2c6 100644
--- a/spec/requests/api/graphql/project/fork_targets_spec.rb
+++ b/spec/requests/api/graphql/project/fork_targets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting a list of fork targets for a project' do
+RSpec.describe 'getting a list of fork targets for a project', feature_category: :source_code_management do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/project/grafana_integration_spec.rb b/spec/requests/api/graphql/project/grafana_integration_spec.rb
index e7534945e7a..1d4f52a8ace 100644
--- a/spec/requests/api/graphql/project/grafana_integration_spec.rb
+++ b/spec/requests/api/graphql/project/grafana_integration_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Getting Grafana Integration' do
+RSpec.describe 'Getting Grafana Integration', feature_category: :metrics do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb
index 544d2d7bd95..7587b227d9f 100644
--- a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb
+++ b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting incident timeline events' do
+RSpec.describe 'getting incident timeline events', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
index 0444ce43c22..5ccf5c1999a 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)' do
+RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)',
+feature_category: :design_management do
include GraphqlHelpers
include DesignManagementTestHelpers
@@ -67,7 +68,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
query_graphql_field(:design_at_version, dav_params, 'id filename')
end
- shared_examples :finds_dav do
+ shared_examples 'finds dav' do
it 'finds all the designs as of the given version' do
post_query
@@ -88,19 +89,19 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
context 'by ID' do
let(:dav_params) { { id: global_id_of(design_at_version) } }
- include_examples :finds_dav
+ include_examples 'finds dav'
end
context 'by filename' do
let(:dav_params) { { filename: design.filename } }
- include_examples :finds_dav
+ include_examples 'finds dav'
end
context 'by design_id' do
let(:dav_params) { { design_id: global_id_of(design) } }
- include_examples :finds_dav
+ include_examples 'finds dav'
end
end
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 46fd65db1c5..a15e4c1e792 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Getting versions related to an issue' do
+RSpec.describe 'Getting versions related to an issue', feature_category: :design_management do
include GraphqlHelpers
include DesignManagementTestHelpers
diff --git a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
index 965534654ea..3765899daf2 100644
--- a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
+++ b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Getting designs related to an issue' do
+RSpec.describe 'Getting designs related to an issue', feature_category: :design_management do
include GraphqlHelpers
include DesignManagementTestHelpers
diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
index 3b1eb0b4b02..69ca7030292 100644
--- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
+++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Getting designs related to an issue' do
+RSpec.describe 'Getting designs related to an issue', feature_category: :design_management do
include GraphqlHelpers
include DesignManagementTestHelpers
diff --git a/spec/requests/api/graphql/project/issue/notes_spec.rb b/spec/requests/api/graphql/project/issue/notes_spec.rb
index 97f5261ef1d..0c7f042510e 100644
--- a/spec/requests/api/graphql/project/issue/notes_spec.rb
+++ b/spec/requests/api/graphql/project/issue/notes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting notes for an issue' do
+RSpec.describe 'getting notes for an issue', feature_category: :team_planning do
include GraphqlHelpers
let(:noteable) { create(:issue) }
diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb
index 2415e9ef60f..bc90f9e89e6 100644
--- a/spec/requests/api/graphql/project/issue_spec.rb
+++ b/spec/requests/api/graphql/project/issue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).issue(iid)' do
+RSpec.describe 'Query.project(fullPath).issue(iid)', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 214165cb171..ec5e3c6f0de 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -2,44 +2,92 @@
require 'spec_helper'
-RSpec.describe 'getting an issue list for a project' do
+RSpec.describe 'getting an issue list for a project', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, :public, group: group) }
let_it_be(:current_user) { create(:user) }
let_it_be(:another_user) { create(:user).tap { |u| group.add_reporter(u) } }
- let_it_be(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) }
- let_it_be(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) }
+ let_it_be(:milestone1) { create(:milestone, project: project, due_date: 10.days.from_now) }
+ let_it_be(:milestone2) { create(:milestone, project: project, due_date: 20.days.from_now) }
+ let_it_be(:milestone3) { create(:milestone, project: project, due_date: 30.days.from_now) }
+ let_it_be(:milestone4) { create(:milestone, project: project, due_date: 40.days.from_now) }
let_it_be(:priority1) { create(:label, project: project, priority: 1) }
let_it_be(:priority2) { create(:label, project: project, priority: 5) }
let_it_be(:priority3) { create(:label, project: project, priority: 10) }
- let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true, labels: [priority3]) }
- let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project, title: 'title matching issue i') }
- let_it_be(:issue_c) { create(:issue, project: project, labels: [priority1], milestone: late_milestone) }
- let_it_be(:issue_d) { create(:issue, project: project, labels: [priority2]) }
- let_it_be(:issue_e) { create(:issue, project: project, milestone: early_milestone) }
- let_it_be(:issues, reload: true) { [issue_a, issue_b, issue_c, issue_d, issue_e] }
+ let_it_be(:issue_a) do
+ create(
+ :issue,
+ project: project,
+ discussion_locked: true,
+ labels: [priority3],
+ relative_position: 1000,
+ milestone: milestone4
+ )
+ end
- let(:issue_a_gid) { issue_a.to_global_id.to_s }
- let(:issue_b_gid) { issue_b.to_global_id.to_s }
- let(:issues_data) { graphql_data['project']['issues']['nodes'] }
- let(:issue_filter_params) { {} }
+ let_it_be(:issue_b) do
+ create(
+ :issue,
+ :with_alert,
+ project: project,
+ title: 'title matching issue i',
+ due_date: 3.days.ago,
+ relative_position: 3000,
+ labels: [priority2, priority3],
+ milestone: milestone1
+ )
+ end
- let(:fields) do
- <<~QUERY
- nodes {
- #{all_graphql_fields_for('issues'.classify)}
- }
- QUERY
+ let_it_be(:issue_c) do
+ create(
+ :issue,
+ project: project,
+ labels: [priority1],
+ milestone: milestone2,
+ due_date: 1.day.ago,
+ relative_position: nil
+ )
+ end
+
+ let_it_be(:issue_d) do
+ create(:issue,
+ project: project,
+ labels: [priority2],
+ due_date: 3.days.from_now,
+ relative_position: 5000,
+ milestone: milestone3
+ )
+ end
+
+ let_it_be(:issue_e) do
+ create(
+ :issue,
+ :confidential,
+ project: project,
+ due_date: 1.day.from_now,
+ relative_position: nil
+ )
end
+ let_it_be(:issues, reload: true) { [issue_a, issue_b, issue_c, issue_d, issue_e] }
+
+ let(:issue_nodes_path) { %w[project issues nodes] }
+ let(:issue_filter_params) { {} }
+
# All new specs should be added to the shared example if the change also
# affects the `issues` query at the root level of the API.
# Shared example also used in spec/requests/api/graphql/issues_spec.rb
it_behaves_like 'graphql issue list request spec' do
- subject(:post_query) { post_graphql(query, current_user: current_user) }
+ let_it_be(:external_user) { create(:user) }
+
+ let(:public_projects) { [project] }
+
+ before_all do
+ group.add_developer(current_user)
+ end
# filters
let(:expected_negated_assignee_issues) { [issue_b, issue_c, issue_d, issue_e] }
@@ -50,24 +98,31 @@ RSpec.describe 'getting an issue list for a project' do
let(:unlocked_discussion_issues) { [issue_b, issue_c, issue_d, issue_e] }
let(:search_title_term) { 'matching issue' }
let(:title_search_issue) { issue_b }
+ let(:confidential_issues) { [issue_e] }
+ let(:non_confidential_issues) { [issue_a, issue_b, issue_c, issue_d] }
+ let(:public_non_confidential_issues) { non_confidential_issues }
# sorting
let(:data_path) { [:project, :issues] }
- let(:expected_severity_sorted_asc) { [issue_c, issue_a, issue_b, issue_e, issue_d] }
- let(:expected_priority_sorted_asc) { [issue_e, issue_c, issue_d, issue_a, issue_b] }
- let(:expected_priority_sorted_desc) { [issue_c, issue_e, issue_a, issue_d, issue_b] }
+ let(:expected_priority_sorted_asc) { [issue_b, issue_c, issue_d, issue_a, issue_e] }
+ let(:expected_priority_sorted_desc) { [issue_a, issue_d, issue_c, issue_b, issue_e] }
+ let(:expected_due_date_sorted_desc) { [issue_d, issue_e, issue_c, issue_b, issue_a] }
+ let(:expected_due_date_sorted_asc) { [issue_b, issue_c, issue_e, issue_d, issue_a] }
+ let(:expected_relative_position_sorted_asc) { [issue_a, issue_b, issue_d, issue_c, issue_e] }
+ let(:expected_label_priority_sorted_asc) { [issue_c, issue_d, issue_b, issue_a, issue_e] }
+ let(:expected_label_priority_sorted_desc) { [issue_a, issue_d, issue_b, issue_c, issue_e] }
+ let(:expected_milestone_sorted_asc) { [issue_b, issue_c, issue_d, issue_a, issue_e] }
+ let(:expected_milestone_sorted_desc) { [issue_a, issue_d, issue_c, issue_b, issue_e] }
+
+ # N+1 queries
+ let(:same_project_issue1) { issue_a }
+ let(:same_project_issue2) { issue_b }
before_all do
issue_a.assignee_ids = current_user.id
issue_b.assignee_ids = another_user.id
create(:award_emoji, :upvote, user: current_user, awardable: issue_a)
-
- # severity sorting
- create(:issuable_severity, issue: issue_a, severity: :unknown)
- create(:issuable_severity, issue: issue_b, severity: :low)
- create(:issuable_severity, issue: issue_d, severity: :critical)
- create(:issuable_severity, issue: issue_e, severity: :high)
end
def pagination_query(params)
@@ -77,591 +132,10 @@ RSpec.describe 'getting an issue list for a project' do
query_graphql_field(:issues, params, "#{page_info} nodes { id }")
)
end
- end
-
- context 'when limiting the number of results' do
- let(:query) do
- <<~GQL
- query($path: ID!, $n: Int) {
- project(fullPath: $path) {
- issues(first: $n) { #{fields} }
- }
- }
- GQL
- end
-
- let(:issue_limit) { 1 }
- let(:variables) do
- { path: project.full_path, n: issue_limit }
- end
-
- it_behaves_like 'a working graphql query' do
- before do
- post_graphql(query, current_user: current_user, variables: variables)
- end
-
- it 'only returns N issues' do
- expect(issues_data.size).to eq(issue_limit)
- end
- end
-
- context 'when no limit is provided' do
- let(:issue_limit) { nil }
-
- it 'returns all issues' do
- post_graphql(query, current_user: current_user, variables: variables)
-
- expect(issues_data.size).to be > 1
- end
- end
-
- it 'is expected to check permissions on the first issue only' do
- allow(Ability).to receive(:allowed?).and_call_original
- # Newest first, we only want to see the newest checked
- expect(Ability).not_to receive(:allowed?).with(current_user, :read_issue, issues.first)
-
- post_graphql(query, current_user: current_user, variables: variables)
- end
- end
-
- context 'when the user does not have access to the issue' do
- it 'returns nil' do
- project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
-
- post_graphql(query)
-
- expect(issues_data).to eq([])
- end
- end
-
- context 'when there is a confidential issue' do
- let_it_be(:confidential_issue) do
- create(:issue, :confidential, project: project)
- end
-
- let(:confidential_issue_gid) { confidential_issue.to_global_id.to_s }
-
- context 'when the user cannot see confidential issues' do
- it 'returns issues without confidential issues' do
- post_graphql(query, current_user: current_user)
-
- expect(issues_data.size).to eq(5)
-
- issues_data.each do |issue|
- expect(issue['confidential']).to eq(false)
- end
- end
-
- context 'filtering for confidential issues' do
- let(:issue_filter_params) { { confidential: true } }
-
- it 'returns no issues' do
- post_graphql(query, current_user: current_user)
-
- expect(issues_data.size).to eq(0)
- end
- end
-
- context 'filtering for non-confidential issues' do
- let(:issue_filter_params) { { confidential: false } }
-
- it 'returns correctly filtered issues' do
- post_graphql(query, current_user: current_user)
-
- expect(issue_ids).to match_array(issues.map { |i| i.to_gid.to_s })
- end
- end
- end
-
- context 'when the user can see confidential issues' do
- before do
- project.add_developer(current_user)
- end
-
- it 'returns issues with confidential issues' do
- post_graphql(query, current_user: current_user)
-
- expect(issues_data.size).to eq(6)
-
- confidentials = issues_data.map do |issue|
- issue['confidential']
- end
-
- expect(confidentials).to contain_exactly(true, false, false, false, false, false)
- end
-
- context 'filtering for confidential issues' do
- let(:issue_filter_params) { { confidential: true } }
-
- it 'returns correctly filtered issues' do
- post_graphql(query, current_user: current_user)
-
- expect(issue_ids).to contain_exactly(confidential_issue_gid)
- end
- end
-
- context 'filtering for non-confidential issues' do
- let(:issue_filter_params) { { confidential: false } }
-
- it 'returns correctly filtered issues' do
- post_graphql(query, current_user: current_user)
-
- expect(issue_ids).to match_array([issue_a, issue_b, issue_c, issue_d, issue_e].map { |i| i.to_gid.to_s })
- end
- end
- end
- end
-
- describe 'sorting and pagination' do
- let_it_be(:sort_project) { create(:project, :public) }
- let_it_be(:data_path) { [:project, :issues] }
-
- def pagination_query(params)
- graphql_query_for(
- :project,
- { full_path: sort_project.full_path },
- query_graphql_field(:issues, params, "#{page_info} nodes { iid }")
- )
- end
- def pagination_results_data(data)
- data.map { |issue| issue['iid'].to_i }
+ def post_query(request_user = current_user)
+ post_graphql(query, current_user: request_user)
end
-
- # rubocop:disable RSpec/MultipleMemoizedHelpers
- context 'when sorting by due date' do
- let_it_be(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) }
- let_it_be(:due_issue2) { create(:issue, project: sort_project, due_date: nil) }
- let_it_be(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) }
- let_it_be(:due_issue4) { create(:issue, project: sort_project, due_date: nil) }
- let_it_be(:due_issue5) { create(:issue, project: sort_project, due_date: 1.day.ago) }
-
- context 'when ascending' do
- it_behaves_like 'sorted paginated query' do
- let(:sort_param) { :DUE_DATE_ASC }
- let(:first_param) { 2 }
- let(:all_records) { [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid] }
- end
- end
-
- context 'when descending' do
- it_behaves_like 'sorted paginated query' do
- let(:sort_param) { :DUE_DATE_DESC }
- let(:first_param) { 2 }
- let(:all_records) { [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid] }
- end
- end
- end
-
- context 'when sorting by relative position' do
- let_it_be(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) }
- let_it_be(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) }
- let_it_be(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) }
- let_it_be(:relative_issue4) { create(:issue, project: sort_project, relative_position: nil) }
- let_it_be(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) }
-
- context 'when ascending' do
- it_behaves_like 'sorted paginated query', is_reversible: true do
- let(:sort_param) { :RELATIVE_POSITION_ASC }
- let(:first_param) { 2 }
- let(:all_records) do
- [
- relative_issue5.iid, relative_issue3.iid, relative_issue1.iid,
- relative_issue2.iid, relative_issue4.iid
- ]
- end
- end
- end
- end
-
- context 'when sorting by label priority' do
- let_it_be(:label1) { create(:label, project: sort_project, priority: 1) }
- let_it_be(:label2) { create(:label, project: sort_project, priority: 5) }
- let_it_be(:label3) { create(:label, project: sort_project, priority: 10) }
- let_it_be(:label_issue1) { create(:issue, project: sort_project, labels: [label1]) }
- let_it_be(:label_issue2) { create(:issue, project: sort_project, labels: [label2]) }
- let_it_be(:label_issue3) { create(:issue, project: sort_project, labels: [label1, label3]) }
- let_it_be(:label_issue4) { create(:issue, project: sort_project) }
-
- context 'when ascending' do
- it_behaves_like 'sorted paginated query' do
- let(:sort_param) { :LABEL_PRIORITY_ASC }
- let(:first_param) { 2 }
- let(:all_records) { [label_issue3.iid, label_issue1.iid, label_issue2.iid, label_issue4.iid] }
- end
- end
-
- context 'when descending' do
- it_behaves_like 'sorted paginated query' do
- let(:sort_param) { :LABEL_PRIORITY_DESC }
- let(:first_param) { 2 }
- let(:all_records) { [label_issue2.iid, label_issue3.iid, label_issue1.iid, label_issue4.iid] }
- end
- end
- end
- # rubocop:enable RSpec/MultipleMemoizedHelpers
-
- context 'when sorting by milestone due date' do
- let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) }
- let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) }
- let_it_be(:milestone_issue1) { create(:issue, project: sort_project) }
- let_it_be(:milestone_issue2) { create(:issue, project: sort_project, milestone: early_milestone) }
- let_it_be(:milestone_issue3) { create(:issue, project: sort_project, milestone: late_milestone) }
-
- context 'when ascending' do
- it_behaves_like 'sorted paginated query' do
- let(:sort_param) { :MILESTONE_DUE_ASC }
- let(:first_param) { 2 }
- let(:all_records) { [milestone_issue2.iid, milestone_issue3.iid, milestone_issue1.iid] }
- end
- end
-
- context 'when descending' do
- it_behaves_like 'sorted paginated query' do
- let(:sort_param) { :MILESTONE_DUE_DESC }
- let(:first_param) { 2 }
- let(:all_records) { [milestone_issue3.iid, milestone_issue2.iid, milestone_issue1.iid] }
- end
- end
- end
- end
-
- context 'when fetching alert management alert' do
- let(:fields) do
- <<~QUERY
- nodes {
- iid
- alertManagementAlert {
- title
- }
- alertManagementAlerts {
- nodes {
- title
- }
- }
- }
- QUERY
- end
-
- # Alerts need to have developer permission and above
- before do
- project.add_developer(current_user)
- end
-
- it 'avoids N+1 queries' do
- control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
-
- create(:alert_management_alert, :with_incident, project: project)
-
- expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
- end
-
- it 'returns the alert data' do
- post_graphql(query, current_user: current_user)
-
- alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlert', 'title') }
- expected_titles = issues.map { |issue| issue.alert_management_alert&.title }
-
- expect(alert_titles).to contain_exactly(*expected_titles)
- end
-
- it 'returns the alerts data' do
- post_graphql(query, current_user: current_user)
-
- alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlerts', 'nodes') }
- expected_titles = issues.map do |issue|
- issue.alert_management_alerts.map { |alert| { 'title' => alert.title } }
- end
-
- expect(alert_titles).to contain_exactly(*expected_titles)
- end
- end
-
- context 'when fetching customer_relations_contacts' do
- let(:fields) do
- <<~QUERY
- nodes {
- id
- customerRelationsContacts {
- nodes {
- firstName
- }
- }
- }
- QUERY
- end
-
- def clean_state_query
- run_with_clean_state(query, context: { current_user: current_user })
- end
-
- it 'avoids N+1 queries' do
- create(:issue_customer_relations_contact, :for_issue, issue: issue_a)
-
- control = ActiveRecord::QueryRecorder.new(skip_cached: false) { clean_state_query }
-
- create(:issue_customer_relations_contact, :for_issue, issue: issue_a)
-
- expect { clean_state_query }.not_to exceed_all_query_limit(control)
- end
- end
-
- context 'when fetching labels' do
- let(:fields) do
- <<~QUERY
- nodes {
- id
- labels {
- nodes {
- id
- }
- }
- }
- QUERY
- end
-
- before do
- issues.each do |issue|
- # create a label for each issue we have to properly test N+1
- label = create(:label, project: project)
- issue.update!(labels: [label])
- end
- end
-
- def response_label_ids(response_data)
- response_data.map do |node|
- node['labels']['nodes'].map { |u| u['id'] }
- end.flatten
- end
-
- def labels_as_global_ids(issues)
- issues.map(&:labels).flatten.map(&:to_global_id).map(&:to_s)
- end
-
- it 'avoids N+1 queries', :aggregate_failures do
- control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
- expect(issues_data.count).to eq(5)
- expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(issues))
-
- new_issues = issues + [create(:issue, project: project, labels: [create(:label, project: project)])]
-
- expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
- # graphql_data is memoized (see spec/support/helpers/graphql_helpers.rb)
- # so we have to parse the body ourselves the second time
- issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['nodes']
- expect(issues_data.count).to eq(6)
- expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(new_issues))
- end
- end
-
- context 'when fetching assignees' do
- let(:fields) do
- <<~QUERY
- nodes {
- id
- assignees {
- nodes {
- id
- }
- }
- }
- QUERY
- end
-
- before do
- issues.each do |issue|
- # create an assignee for each issue we have to properly test N+1
- assignee = create(:user)
- issue.update!(assignees: [assignee])
- end
- end
-
- def response_assignee_ids(response_data)
- response_data.map do |node|
- node['assignees']['nodes'].map { |node| node['id'] }
- end.flatten
- end
-
- def assignees_as_global_ids(issues)
- issues.map(&:assignees).flatten.map(&:to_global_id).map(&:to_s)
- end
-
- it 'avoids N+1 queries', :aggregate_failures do
- control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
- expect(issues_data.count).to eq(5)
- expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(issues))
-
- new_issues = issues + [create(:issue, project: project, assignees: [create(:user)])]
-
- expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
- # graphql_data is memoized (see spec/support/helpers/graphql_helpers.rb)
- # so we have to parse the body ourselves the second time
- issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['nodes']
- expect(issues_data.count).to eq(6)
- expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(new_issues))
- end
- end
-
- context 'when fetching escalation status' do
- let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) }
-
- let(:statuses) { issue_data.to_h { |issue| [issue['iid'], issue['escalationStatus']] } }
- let(:fields) do
- <<~QUERY
- nodes {
- id
- escalationStatus
- }
- QUERY
- end
-
- before do
- issue_a.update!(issue_type: Issue.issue_types[:incident])
- end
-
- it 'returns the escalation status values' do
- post_graphql(query, current_user: current_user)
-
- statuses = issues_data.map { |issue| issue['escalationStatus'] }
-
- expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil, nil, nil, nil)
- end
-
- it 'avoids N+1 queries', :aggregate_failures do
- base_count = ActiveRecord::QueryRecorder.new { run_with_clean_state(query, context: { current_user: current_user }) }
-
- new_incident = create(:incident, project: project)
- create(:incident_management_issuable_escalation_status, issue: new_incident)
-
- expect { run_with_clean_state(query, context: { current_user: current_user }) }.not_to exceed_query_limit(base_count)
- end
- end
-
- describe 'N+1 query checks' do
- let(:extra_iid_for_second_query) { issue_b.iid.to_s }
- let(:search_params) { { iids: [issue_a.iid.to_s] } }
-
- def execute_query
- query = graphql_query_for(
- :project,
- { full_path: project.full_path },
- query_graphql_field(
- :issues, search_params,
- query_graphql_field(:nodes, nil, requested_fields)
- )
- )
- post_graphql(query, current_user: current_user)
- end
-
- context 'when requesting `user_notes_count`' do
- let(:requested_fields) { [:user_notes_count] }
-
- before do
- create_list(:note_on_issue, 2, noteable: issue_a, project: project)
- create(:note_on_issue, noteable: issue_b, project: project)
- end
-
- include_examples 'N+1 query check'
- end
-
- context 'when requesting `user_discussions_count`' do
- let(:requested_fields) { [:user_discussions_count] }
-
- before do
- create_list(:note_on_issue, 2, noteable: issue_a, project: project)
- create(:note_on_issue, noteable: issue_b, project: project)
- end
-
- include_examples 'N+1 query check'
- end
-
- context 'when requesting `merge_requests_count`' do
- let(:requested_fields) { [:merge_requests_count] }
-
- before do
- create_list(:merge_requests_closing_issues, 2, issue: issue_a)
- create_list(:merge_requests_closing_issues, 3, issue: issue_b)
- end
-
- include_examples 'N+1 query check'
- end
-
- context 'when requesting `timelogs`' do
- let(:requested_fields) { 'timelogs { nodes { timeSpent } }' }
-
- before do
- create_list(:issue_timelog, 2, issue: issue_a)
- create(:issue_timelog, issue: issue_b)
- end
-
- include_examples 'N+1 query check'
- end
-
- context 'when requesting `closed_as_duplicate_of`' do
- let(:requested_fields) { 'closedAsDuplicateOf { id }' }
- let(:issue_a_dup) { create(:issue, project: project) }
- let(:issue_b_dup) { create(:issue, project: project) }
-
- before do
- issue_a.update!(duplicated_to_id: issue_a_dup)
- issue_b.update!(duplicated_to_id: issue_a_dup)
- end
-
- include_examples 'N+1 query check'
- end
-
- context 'when award emoji votes' do
- let(:requested_fields) { [:upvotes, :downvotes] }
-
- before do
- create_list(:award_emoji, 2, name: 'thumbsup', awardable: issue_a)
- create_list(:award_emoji, 2, name: 'thumbsdown', awardable: issue_b)
- end
-
- include_examples 'N+1 query check'
- end
-
- context 'when requesting participants' do
- let_it_be(:issue_c) { create(:issue, project: project) }
-
- let(:search_params) { { iids: [issue_a.iid.to_s, issue_c.iid.to_s] } }
- let(:requested_fields) { 'participants { nodes { name } }' }
-
- before do
- create(:award_emoji, :upvote, awardable: issue_a)
- create(:award_emoji, :upvote, awardable: issue_b)
- create(:award_emoji, :upvote, awardable: issue_c)
-
- note_with_emoji_a = create(:note_on_issue, noteable: issue_a, project: project)
- note_with_emoji_b = create(:note_on_issue, noteable: issue_b, project: project)
- note_with_emoji_c = create(:note_on_issue, noteable: issue_c, project: project)
-
- create(:award_emoji, :upvote, awardable: note_with_emoji_a)
- create(:award_emoji, :upvote, awardable: note_with_emoji_b)
- create(:award_emoji, :upvote, awardable: note_with_emoji_c)
- end
-
- # Executes 3 extra queries to fetch participant_attrs
- include_examples 'N+1 query check', threshold: 3
- end
-
- context 'when requesting labels' do
- let(:requested_fields) { ['labels { nodes { id } }'] }
-
- before do
- project_labels = create_list(:label, 2, project: project)
- group_labels = create_list(:group_label, 2, group: group)
-
- issue_a.update!(labels: [project_labels.first, group_labels.first].flatten)
- issue_b.update!(labels: [project_labels, group_labels].flatten)
- end
-
- include_examples 'N+1 query check', skip_cached: false
- end
- end
-
- def issue_ids
- graphql_dig_at(issues_data, :id)
end
def query(params = issue_filter_params)
diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb
index 202220f4bf6..821357b6988 100644
--- a/spec/requests/api/graphql/project/jira_import_spec.rb
+++ b/spec/requests/api/graphql/project/jira_import_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'query Jira import data' do
+RSpec.describe 'query Jira import data', feature_category: :integrations do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/project/jira_projects_spec.rb b/spec/requests/api/graphql/project/jira_projects_spec.rb
index 410d5b21505..3cd689deda5 100644
--- a/spec/requests/api/graphql/project/jira_projects_spec.rb
+++ b/spec/requests/api/graphql/project/jira_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'query Jira projects' do
+RSpec.describe 'query Jira projects', feature_category: :integrations do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/project/jira_service_spec.rb b/spec/requests/api/graphql/project/jira_service_spec.rb
index d6abe94b873..23f32d2c2d2 100644
--- a/spec/requests/api/graphql/project/jira_service_spec.rb
+++ b/spec/requests/api/graphql/project/jira_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'query Jira service' do
+RSpec.describe 'query Jira service', feature_category: :integrations do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/project/job_spec.rb b/spec/requests/api/graphql/project/job_spec.rb
index 6edd4cf753f..ba1c8a1f616 100644
--- a/spec/requests/api/graphql/project/job_spec.rb
+++ b/spec/requests/api/graphql/project/job_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project.job' do
+RSpec.describe 'Query.project.job', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/project/jobs_spec.rb b/spec/requests/api/graphql/project/jobs_spec.rb
index 7d0eb203d60..d05d4a2f4b6 100644
--- a/spec/requests/api/graphql/project/jobs_spec.rb
+++ b/spec/requests/api/graphql/project/jobs_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Query.project.jobs' do
+RSpec.describe 'Query.project.jobs', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
diff --git a/spec/requests/api/graphql/project/labels_query_spec.rb b/spec/requests/api/graphql/project/labels_query_spec.rb
index eeaaaaee575..1930a22ad30 100644
--- a/spec/requests/api/graphql/project/labels_query_spec.rb
+++ b/spec/requests/api/graphql/project/labels_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting project label information' do
+RSpec.describe 'getting project label information', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/requests/api/graphql/project/languages_spec.rb b/spec/requests/api/graphql/project/languages_spec.rb
index 6ef500cde41..88a196c3ff4 100644
--- a/spec/requests/api/graphql/project/languages_spec.rb
+++ b/spec/requests/api/graphql/project/languages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project.languages' do
+RSpec.describe 'Project.languages', feature_category: :internationalization do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb b/spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb
index b1ecb32b365..36e148468bc 100644
--- a/spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting notes for a merge request' do
+RSpec.describe 'getting notes for a merge request', feature_category: :code_review do
include GraphqlHelpers
let_it_be(:noteable) { create(:merge_request) }
diff --git a/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb
index 4dc272b5c2e..fb7e46cff8e 100644
--- a/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project.mergeRequests.pipelines' do
+RSpec.describe 'Query.project.mergeRequests.pipelines', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :public, :repository) }
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index 6a59df81405..b7aafdf305a 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting merge request information nested in a project' do
+RSpec.describe 'getting merge request information nested in a project', feature_category: :code_review do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index 2895737ae6f..b3b4c8fe0d5 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting merge request listings nested in a project' do
+RSpec.describe 'getting merge request listings nested in a project', feature_category: :code_review do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/project/milestones_spec.rb b/spec/requests/api/graphql/project/milestones_spec.rb
index a577c367fe5..3b31da77a75 100644
--- a/spec/requests/api/graphql/project/milestones_spec.rb
+++ b/spec/requests/api/graphql/project/milestones_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting milestone listings nested in a project' do
+RSpec.describe 'getting milestone listings nested in a project', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:today) { Time.now.utc.to_date }
diff --git a/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb
index 33e1dbcba27..a7d5cc79f1a 100644
--- a/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb
+++ b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting the packages cleanup policy linked to a project' do
+RSpec.describe 'getting the packages cleanup policy linked to a project', feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
diff --git a/spec/requests/api/graphql/project/packages_spec.rb b/spec/requests/api/graphql/project/packages_spec.rb
index d9ee997eb02..3413b80e8b4 100644
--- a/spec/requests/api/graphql/project/packages_spec.rb
+++ b/spec/requests/api/graphql/project/packages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting a package list for a project' do
+RSpec.describe 'getting a package list for a project', feature_category: :package_registry do
include GraphqlHelpers
let_it_be(:resource) { create(:project, :repository) }
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
index 41915d3cdee..0eeb382510e 100644
--- a/spec/requests/api/graphql/project/pipeline_spec.rb
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting pipeline information nested in a project' do
+RSpec.describe 'getting pipeline information nested in a project', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb
index 97a79ab3b0e..1f1d8027592 100644
--- a/spec/requests/api/graphql/project/project_members_spec.rb
+++ b/spec/requests/api/graphql/project/project_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting project members information' do
+RSpec.describe 'getting project members information', feature_category: :projects do
include GraphqlHelpers
let_it_be(:parent_group) { create(:group, :public) }
diff --git a/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb b/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb
index 39a68d98d84..a13e96eb9d3 100644
--- a/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb
+++ b/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'rendering project pipeline statistics' do
+RSpec.describe 'rendering project pipeline statistics', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/project/project_statistics_spec.rb b/spec/requests/api/graphql/project/project_statistics_spec.rb
index b57c594c64f..d078659b954 100644
--- a/spec/requests/api/graphql/project/project_statistics_spec.rb
+++ b/spec/requests/api/graphql/project/project_statistics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'rendering project statistics' do
+RSpec.describe 'rendering project statistics', feature_category: :project_statistics do
include GraphqlHelpers
let(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/project/recent_issue_boards_query_spec.rb b/spec/requests/api/graphql/project/recent_issue_boards_query_spec.rb
index b3daf86c4af..ae459fd26fb 100644
--- a/spec/requests/api/graphql/project/recent_issue_boards_query_spec.rb
+++ b/spec/requests/api/graphql/project/recent_issue_boards_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting project recent issue boards' do
+RSpec.describe 'getting project recent issue boards', feature_category: :team_planning do
include GraphqlHelpers
it_behaves_like 'querying a GraphQL type recent boards' do
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index c4899dbb71e..477388585ca 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).release(tagName)' do
+RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :release_orchestration do
include GraphqlHelpers
include Presentable
diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb
index c28a6fa7666..aa454349fcf 100644
--- a/spec/requests/api/graphql/project/releases_spec.rb
+++ b/spec/requests/api/graphql/project/releases_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).releases()' do
+RSpec.describe 'Query.project(fullPath).releases()', feature_category: :release_orchestration do
include GraphqlHelpers
let_it_be(:stranger) { create(:user) }
diff --git a/spec/requests/api/graphql/project/repository/blobs_spec.rb b/spec/requests/api/graphql/project/repository/blobs_spec.rb
index ba87f1100f2..a4ee0910d30 100644
--- a/spec/requests/api/graphql/project/repository/blobs_spec.rb
+++ b/spec/requests/api/graphql/project/repository/blobs_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting blobs in a project repository' do
+RSpec.describe 'getting blobs in a project repository', feature_category: :source_code_management do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
diff --git a/spec/requests/api/graphql/project/repository_spec.rb b/spec/requests/api/graphql/project/repository_spec.rb
index b00f64c3db6..9f4d69c7b17 100644
--- a/spec/requests/api/graphql/project/repository_spec.rb
+++ b/spec/requests/api/graphql/project/repository_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting a repository in a project' do
+RSpec.describe 'getting a repository in a project', feature_category: :source_code_management do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
diff --git a/spec/requests/api/graphql/project/runners_spec.rb b/spec/requests/api/graphql/project/runners_spec.rb
new file mode 100644
index 00000000000..7304de7bec6
--- /dev/null
+++ b/spec/requests/api/graphql/project/runners_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project.runners', feature_category: :runner do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+ let_it_be(:instance_runner) { create(:ci_runner, :instance) }
+ let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+ let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+ let_it_be(:other_project) { create(:project, :repository, :public) }
+ let_it_be(:other_project_runner) { create(:ci_runner, :project, projects: [other_project]) }
+
+ let_it_be(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ runners {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+ )
+ end
+
+ context 'when the user is a project admin' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ let(:expected_ids) { [project_runner, group_runner, instance_runner].map { |g| g.to_global_id.to_s } }
+
+ it 'returns all runners available to project' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :runners, :nodes).pluck('id')).to match_array(expected_ids)
+ end
+ end
+
+ context 'when the user is a project developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns no runners' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :runners, :nodes)).to be_empty
+ end
+ end
+
+ context 'when on_demand_scans_runner_tags feature flag is disabled' do
+ before do
+ stub_feature_flags(on_demand_scans_runner_tags: false)
+ end
+
+ it 'returns no runners' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :runners, :nodes)).to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/terraform/state_spec.rb b/spec/requests/api/graphql/project/terraform/state_spec.rb
index 5e207ec0963..1889e7a1064 100644
--- a/spec/requests/api/graphql/project/terraform/state_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/state_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'query a single terraform state' do
+RSpec.describe 'query a single terraform state', feature_category: :infrastructure_as_code do
include GraphqlHelpers
include ::API::Helpers::RelatedResourcesHelpers
diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb
index cc3660bcc6b..25fc07ef509 100644
--- a/spec/requests/api/graphql/project/terraform/states_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/states_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'query terraform states' do
+RSpec.describe 'query terraform states', feature_category: :infrastructure_as_code do
include GraphqlHelpers
include ::API::Helpers::RelatedResourcesHelpers
diff --git a/spec/requests/api/graphql/project/tree/tree_spec.rb b/spec/requests/api/graphql/project/tree/tree_spec.rb
index e63e0d3dd04..77b72bf39a1 100644
--- a/spec/requests/api/graphql/project/tree/tree_spec.rb
+++ b/spec/requests/api/graphql/project/tree/tree_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'getting a tree in a project' do
+RSpec.describe 'getting a tree in a project', feature_category: :source_code_management do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
@@ -166,6 +166,54 @@ RSpec.describe 'getting a tree in a project' do
end
end
+ context 'when the ref points to a SSH-signed commit' do
+ let_it_be(:ref) { 'ssh-signed-commit' }
+ let_it_be(:commit) { project.commit(ref) }
+ let_it_be(:current_user) { create(:user, email: commit.committer_email).tap { |user| project.add_owner(user) } }
+
+ let(:fields) do
+ <<~QUERY
+ tree(path:"#{path}", ref:"#{ref}") {
+ lastCommit {
+ signature {
+ ... on SshSignature {
+ #{all_graphql_fields_for('SshSignature'.classify, max_depth: 2)}
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ let_it_be(:key) do
+ create(:key, user: current_user, key: extract_public_key_from_commit(commit), expires_at: 2.days.from_now)
+ end
+
+ def extract_public_key_from_commit(commit)
+ ssh_commit = Gitlab::Ssh::Commit.new(commit)
+ signature_data = ::SSHData::Signature.parse_pem(ssh_commit.signature_text)
+ signature_data.public_key.openssh
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns the expected signature data' do
+ signature = graphql_data['project']['repository']['tree']['lastCommit']['signature']
+
+ expect(signature['commitSha']).to eq(commit.id)
+ expect(signature['verificationStatus']).to eq('VERIFIED')
+ expect(signature['project']['id']).to eq("gid://gitlab/Project/#{project.id}")
+ expect(signature['user']['id']).to eq("gid://gitlab/User/#{current_user.id}")
+ expect(signature['key']['id']).to eq("gid://gitlab/Key/#{key.id}")
+ expect(signature['key']['title']).to eq(key.title)
+ expect(signature['key']['createdAt']).to be_present
+ expect(signature['key']['expiresAt']).to be_present
+ expect(signature['key']['key']).to match(key.key)
+ end
+ end
+
context 'when current user is nil' do
it 'returns empty project' do
post_graphql(query, current_user: nil)
diff --git a/spec/requests/api/graphql/project/work_item_types_spec.rb b/spec/requests/api/graphql/project/work_item_types_spec.rb
index 3d30baab816..c31a260c4b8 100644
--- a/spec/requests/api/graphql/project/work_item_types_spec.rb
+++ b/spec/requests/api/graphql/project/work_item_types_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting a list of work item types for a project' do
+RSpec.describe 'getting a list of work item types for a project', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:developer) { create(:user) }
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index 6d20799c9ec..a59da706a8a 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting a work item list for a project' do
+RSpec.describe 'getting a work item list for a project', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
@@ -93,6 +93,11 @@ RSpec.describe 'getting a work item list for a project' do
}
... on WorkItemWidgetHierarchy {
parent { id }
+ children {
+ nodes {
+ id
+ }
+ }
}
... on WorkItemWidgetLabels {
labels { nodes { id } }
@@ -112,6 +117,57 @@ RSpec.describe 'getting a work item list for a project' do
end
end
+ context 'when querying WorkItemWidgetHierarchy' do
+ let_it_be(:children) { create_list(:work_item, 3, :task, project: project) }
+ let_it_be(:child_link1) { create(:parent_link, work_item_parent: item1, work_item: children[0]) }
+
+ let(:fields) do
+ <<~GRAPHQL
+ nodes {
+ widgets {
+ type
+ ... on WorkItemWidgetHierarchy {
+ hasChildren
+ parent { id }
+ children { nodes { id } }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'executes limited number of N+1 queries' do
+ post_graphql(query, current_user: current_user) # warm-up
+
+ control = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: current_user)
+ end
+
+ parent_work_items = create_list(:work_item, 2, project: project)
+ create(:parent_link, work_item_parent: parent_work_items[0], work_item: children[1])
+ create(:parent_link, work_item_parent: parent_work_items[1], work_item: children[2])
+
+ # There are 2 extra queries for fetching the children field
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/363569
+ expect { post_graphql(query, current_user: current_user) }
+ .not_to exceed_query_limit(control).with_threshold(2)
+ end
+
+ it 'avoids N+1 queries when children are added to a work item' do
+ post_graphql(query, current_user: current_user) # warm-up
+
+ control = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: current_user)
+ end
+
+ create(:parent_link, work_item_parent: item1, work_item: children[1])
+ create(:parent_link, work_item_parent: item1, work_item: children[2])
+
+ expect { post_graphql(query, current_user: current_user) }
+ .not_to exceed_query_limit(control)
+ end
+ end
+
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
@@ -188,6 +244,60 @@ RSpec.describe 'getting a work item list for a project' do
end
end
+ describe 'fetching work item notes widget' do
+ let(:item_filter_params) { { iid: item2.iid.to_s } }
+ let(:fields) do
+ <<~GRAPHQL
+ edges {
+ node {
+ 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
+
+ before do
+ create_notes(item1, "some note1")
+ create_notes(item2, "some note2")
+ end
+
+ shared_examples 'fetches work item notes' do |user_comments_count:, system_notes_count:|
+ it "fetches notes" do
+ post_graphql(query, current_user: current_user)
+
+ all_widgets = graphql_dig_at(items_data, :node, :widgets)
+ notes_widget = all_widgets.find { |x| x["type"] == "NOTES" }
+
+ all_notes = graphql_dig_at(notes_widget["all_notes"], :nodes)
+ system_notes = graphql_dig_at(notes_widget["system"], :nodes)
+ comments = graphql_dig_at(notes_widget["comments"], :nodes)
+
+ expect(comments.count).to eq(user_comments_count)
+ expect(system_notes.count).to eq(system_notes_count)
+ expect(all_notes.count).to eq(user_comments_count + system_notes_count)
+ end
+ end
+
+ context 'when user has permission to view internal notes' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'fetches work item notes', user_comments_count: 2, system_notes_count: 5
+ end
+
+ context 'when user cannot view internal notes' do
+ it_behaves_like 'fetches work item notes', user_comments_count: 1, system_notes_count: 5
+ end
+ end
+
def item_ids
graphql_dig_at(items_data, :node, :id)
end
@@ -199,4 +309,26 @@ RSpec.describe 'getting a work item list for a project' do
query_graphql_field('workItems', params, fields)
)
end
+
+ def create_notes(work_item, note_body)
+ create(:note, system: true, project: work_item.project, noteable: work_item)
+
+ disc_start = create(:discussion_note_on_issue, noteable: work_item, project: work_item.project, note: note_body)
+ create(:note,
+ discussion_id: disc_start.discussion_id, noteable: work_item,
+ project: work_item.project, note: "reply on #{note_body}")
+
+ create(:resource_label_event, user: current_user, issue: work_item, label: label1, action: 'add')
+ create(:resource_label_event, user: current_user, issue: work_item, label: label1, action: 'remove')
+
+ create(:resource_milestone_event, issue: work_item, milestone: milestone1, action: 'add')
+ create(:resource_milestone_event, issue: work_item, milestone: milestone1, action: 'remove')
+
+ # confidential notes are currently available only on issues and epics
+ conf_disc_start = create(:discussion_note_on_issue, :confidential,
+ noteable: work_item, project: work_item.project, note: "confidential #{note_body}")
+ create(:note, :confidential,
+ discussion_id: conf_disc_start.discussion_id, noteable: work_item,
+ project: work_item.project, note: "reply on confidential #{note_body}")
+ end
end
diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb
index d1b990629a1..281a08e6548 100644
--- a/spec/requests/api/graphql/project_query_spec.rb
+++ b/spec/requests/api/graphql/project_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting project information' do
+RSpec.describe 'getting project information', feature_category: :projects do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb
index 359c599cd3a..2b9d66ec744 100644
--- a/spec/requests/api/graphql/query_spec.rb
+++ b/spec/requests/api/graphql/query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query' do
+RSpec.describe 'Query', feature_category: :not_owned do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/read_only_spec.rb b/spec/requests/api/graphql/read_only_spec.rb
index d2a45603886..aec8d3151c8 100644
--- a/spec/requests/api/graphql/read_only_spec.rb
+++ b/spec/requests/api/graphql/read_only_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Requests on a read-only node' do
+RSpec.describe 'Requests on a read-only node', feature_category: :database do
context 'when db is read-only' do
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
diff --git a/spec/requests/api/graphql/snippets_spec.rb b/spec/requests/api/graphql/snippets_spec.rb
index 9edd805678a..3cd95b0841c 100644
--- a/spec/requests/api/graphql/snippets_spec.rb
+++ b/spec/requests/api/graphql/snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'snippets' do
+RSpec.describe 'snippets', feature_category: :source_code_management do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/tasks/task_completion_status_spec.rb b/spec/requests/api/graphql/tasks/task_completion_status_spec.rb
index 5f4d2aec718..ea89487c176 100644
--- a/spec/requests/api/graphql/tasks/task_completion_status_spec.rb
+++ b/spec/requests/api/graphql/tasks/task_completion_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting task completion status information' do
+RSpec.describe 'getting task completion status information', feature_category: :team_planning do
include GraphqlHelpers
description_0_done = '- [ ] task 1\n- [ ] task 2'
diff --git a/spec/requests/api/graphql/terraform/state/delete_spec.rb b/spec/requests/api/graphql/terraform/state/delete_spec.rb
index ba0619ea611..f4af402492c 100644
--- a/spec/requests/api/graphql/terraform/state/delete_spec.rb
+++ b/spec/requests/api/graphql/terraform/state/delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'delete a terraform state' do
+RSpec.describe 'delete a terraform state', feature_category: :infrastructure_as_code do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/terraform/state/lock_spec.rb b/spec/requests/api/graphql/terraform/state/lock_spec.rb
index e4d3b6336ab..4219f4f4651 100644
--- a/spec/requests/api/graphql/terraform/state/lock_spec.rb
+++ b/spec/requests/api/graphql/terraform/state/lock_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'lock a terraform state' do
+RSpec.describe 'lock a terraform state', feature_category: :infrastructure_as_code do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/terraform/state/unlock_spec.rb b/spec/requests/api/graphql/terraform/state/unlock_spec.rb
index e90730f2d8f..84ccc09711d 100644
--- a/spec/requests/api/graphql/terraform/state/unlock_spec.rb
+++ b/spec/requests/api/graphql/terraform/state/unlock_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'unlock a terraform state' do
+RSpec.describe 'unlock a terraform state', feature_category: :infrastructure_as_code do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/todo_query_spec.rb b/spec/requests/api/graphql/todo_query_spec.rb
index 7fe19448083..b8ce065dbbb 100644
--- a/spec/requests/api/graphql/todo_query_spec.rb
+++ b/spec/requests/api/graphql/todo_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Todo Query' do
+RSpec.describe 'Todo Query', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:current_user) { nil }
diff --git a/spec/requests/api/graphql/usage_trends_measurements_spec.rb b/spec/requests/api/graphql/usage_trends_measurements_spec.rb
index 78a4321f522..68b97fbb130 100644
--- a/spec/requests/api/graphql/usage_trends_measurements_spec.rb
+++ b/spec/requests/api/graphql/usage_trends_measurements_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'UsageTrendsMeasurements' do
+RSpec.describe 'UsageTrendsMeasurements', feature_category: :devops_reports do
include GraphqlHelpers
let(:current_user) { create(:user, :admin) }
diff --git a/spec/requests/api/graphql/user/group_member_query_spec.rb b/spec/requests/api/graphql/user/group_member_query_spec.rb
index e47cef8cc37..d09cb319877 100644
--- a/spec/requests/api/graphql/user/group_member_query_spec.rb
+++ b/spec/requests/api/graphql/user/group_member_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'GroupMember' do
+RSpec.describe 'GroupMember', feature_category: :subgroups do
include GraphqlHelpers
let_it_be(:member) { create(:group_member, :developer) }
diff --git a/spec/requests/api/graphql/user/project_member_query_spec.rb b/spec/requests/api/graphql/user/project_member_query_spec.rb
index 01827e94d5d..1baa7815793 100644
--- a/spec/requests/api/graphql/user/project_member_query_spec.rb
+++ b/spec/requests/api/graphql/user/project_member_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'ProjectMember' do
+RSpec.describe 'ProjectMember', feature_category: :subgroups do
include GraphqlHelpers
let_it_be(:member) { create(:project_member, :developer) }
diff --git a/spec/requests/api/graphql/user/starred_projects_query_spec.rb b/spec/requests/api/graphql/user/starred_projects_query_spec.rb
index 75a17ed34c4..7d4284300d8 100644
--- a/spec/requests/api/graphql/user/starred_projects_query_spec.rb
+++ b/spec/requests/api/graphql/user/starred_projects_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Getting starredProjects of the user' do
+RSpec.describe 'Getting starredProjects of the user', feature_category: :projects do
include GraphqlHelpers
let(:query) do
diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb
index 8f286180617..ca319ed1b2e 100644
--- a/spec/requests/api/graphql/user_query_spec.rb
+++ b/spec/requests/api/graphql/user_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting user information' do
+RSpec.describe 'getting user information', feature_category: :user_management do
include GraphqlHelpers
let(:query) do
diff --git a/spec/requests/api/graphql/user_spec.rb b/spec/requests/api/graphql/user_spec.rb
index a3b2b750bc3..2e1e4971767 100644
--- a/spec/requests/api/graphql/user_spec.rb
+++ b/spec/requests/api/graphql/user_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User' do
+RSpec.describe 'User', feature_category: :users do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/users_spec.rb b/spec/requests/api/graphql/users_spec.rb
index 79ee3c2cb57..83c360fdaf8 100644
--- a/spec/requests/api/graphql/users_spec.rb
+++ b/spec/requests/api/graphql/users_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Users' do
+RSpec.describe 'Users', feature_category: :user_management do
include GraphqlHelpers
let_it_be(:user0) { create(:user, created_at: 1.day.ago) }
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index a55de6adfb2..df7dbaea420 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.work_item(id)' do
+RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:developer) { create(:user) }
@@ -116,6 +116,7 @@ RSpec.describe 'Query.work_item(id)' do
id
}
}
+ hasChildren
}
}
GRAPHQL
@@ -132,7 +133,8 @@ RSpec.describe 'Query.work_item(id)' do
[
hash_including('id' => child_link1.work_item.to_gid.to_s),
hash_including('id' => child_link2.work_item.to_gid.to_s)
- ]) }
+ ]) },
+ 'hasChildren' => true
)
)
)
@@ -165,7 +167,8 @@ RSpec.describe 'Query.work_item(id)' do
'children' => { 'nodes' => match_array(
[
hash_including('id' => child_link1.work_item.to_gid.to_s)
- ]) }
+ ]) },
+ 'hasChildren' => true
)
)
)
@@ -183,7 +186,8 @@ RSpec.describe 'Query.work_item(id)' do
hash_including(
'type' => 'HIERARCHY',
'parent' => hash_including('id' => parent_link.work_item_parent.to_gid.to_s),
- 'children' => { 'nodes' => match_array([]) }
+ 'children' => { 'nodes' => match_array([]) },
+ 'hasChildren' => false
)
)
)
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
index 1c1ae73ddfe..d7724371cce 100644
--- a/spec/requests/api/graphql_spec.rb
+++ b/spec/requests/api/graphql_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'GraphQL' do
+RSpec.describe 'GraphQL', feature_category: :not_owned do
include GraphqlHelpers
include AfterNextHelpers
@@ -25,12 +25,12 @@ RSpec.describe 'GraphQL' do
"query_analysis.used_fields" => ['Query.echo'],
"query_analysis.used_deprecated_fields" => [],
# query_fingerprint starts with operation name
- query_fingerprint: %r{^anonymous\/},
+ query_fingerprint: %r{^anonymous/},
duration_s: kind_of(Numeric),
trace_type: 'execute_query',
operation_name: nil,
# operation_fingerprint starts with operation name
- operation_fingerprint: %r{^anonymous\/},
+ operation_fingerprint: %r{^anonymous/},
is_mutation: false,
variables: variables.to_s,
query_string: query
diff --git a/spec/requests/api/group_avatar_spec.rb b/spec/requests/api/group_avatar_spec.rb
index 50379d29b09..9a0e79ee9f8 100644
--- a/spec/requests/api/group_avatar_spec.rb
+++ b/spec/requests/api/group_avatar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupAvatar do
+RSpec.describe API::GroupAvatar, feature_category: :subgroups do
def avatar_path(group)
"/groups/#{ERB::Util.url_encode(group.full_path)}/avatar"
end
diff --git a/spec/requests/api/group_boards_spec.rb b/spec/requests/api/group_boards_spec.rb
index cc110aa4017..01f0e6e2061 100644
--- a/spec/requests/api/group_boards_spec.rb
+++ b/spec/requests/api/group_boards_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupBoards do
+RSpec.describe API::GroupBoards, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:non_member) { create(:user) }
let_it_be(:guest) { create(:user) }
diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb
index 8e127bf0710..68c3af01e56 100644
--- a/spec/requests/api/group_clusters_spec.rb
+++ b/spec/requests/api/group_clusters_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupClusters do
+RSpec.describe API::GroupClusters, feature_category: :kubernetes_management do
include KubernetesHelpers
let(:current_user) { create(:user) }
diff --git a/spec/requests/api/group_container_repositories_spec.rb b/spec/requests/api/group_container_repositories_spec.rb
index 82daab0e5e8..cd88b060a3a 100644
--- a/spec/requests/api/group_container_repositories_spec.rb
+++ b/spec/requests/api/group_container_repositories_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupContainerRepositories do
+RSpec.describe API::GroupContainerRepositories, feature_category: :container_registry do
let_it_be(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, :private, group: group) }
let_it_be(:reporter) { create(:user) }
@@ -35,7 +35,9 @@ RSpec.describe API::GroupContainerRepositories do
describe 'GET /groups/:id/registry/repositories' do
let(:url) { "/groups/#{group.id}/registry/repositories" }
- let(:snowplow_gitlab_standard_context) { { user: api_user, namespace: group } }
+ let(:snowplow_gitlab_standard_context) do
+ { user: api_user, namespace: group, property: 'i_package_container_user' }
+ end
subject { get api(url, api_user) }
diff --git a/spec/requests/api/group_debian_distributions_spec.rb b/spec/requests/api/group_debian_distributions_spec.rb
index 21c5f2f09a0..57b481e4f9f 100644
--- a/spec/requests/api/group_debian_distributions_spec.rb
+++ b/spec/requests/api/group_debian_distributions_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::GroupDebianDistributions do
+RSpec.describe API::GroupDebianDistributions, feature_category: :package_registry do
include HttpBasicAuthHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb
index 83c34204c78..565365506a7 100644
--- a/spec/requests/api/group_export_spec.rb
+++ b/spec/requests/api/group_export_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupExport do
+RSpec.describe API::GroupExport, feature_category: :importers do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/group_import_spec.rb b/spec/requests/api/group_import_spec.rb
index efad6334518..07c21c93585 100644
--- a/spec/requests/api/group_import_spec.rb
+++ b/spec/requests/api/group_import_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupImport do
+RSpec.describe API::GroupImport, feature_category: :importers do
include WorkhorseHelpers
include_context 'workhorse headers'
@@ -198,12 +198,6 @@ RSpec.describe API::GroupImport do
include_examples 'when all params are correct'
include_examples 'when some params are missing'
end
-
- it "doesn't attempt to migrate file to object storage" do
- expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
-
- subject
- end
end
context 'with object storage enabled' do
diff --git a/spec/requests/api/group_labels_spec.rb b/spec/requests/api/group_labels_spec.rb
index 34533da53dd..1dd90413d35 100644
--- a/spec/requests/api/group_labels_spec.rb
+++ b/spec/requests/api/group_labels_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupLabels do
+RSpec.describe API::GroupLabels, feature_category: :team_planning do
let_it_be(:valid_group_label_title_1) { 'Label foo & bar:subgroup::v.1' }
let_it_be(:valid_group_label_title_1_esc) { ERB::Util.url_encode(valid_group_label_title_1) }
let_it_be(:valid_group_label_title_2) { 'Bar & foo:subgroup::v.2' }
diff --git a/spec/requests/api/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb
index da84e98b905..91f64d02d43 100644
--- a/spec/requests/api/group_milestones_spec.rb
+++ b/spec/requests/api/group_milestones_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupMilestones do
+RSpec.describe API::GroupMilestones, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, namespace: group) }
diff --git a/spec/requests/api/group_packages_spec.rb b/spec/requests/api/group_packages_spec.rb
index a2b0b35c76a..0b4f6130132 100644
--- a/spec/requests/api/group_packages_spec.rb
+++ b/spec/requests/api/group_packages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupPackages do
+RSpec.describe API::GroupPackages, feature_category: :package_registry do
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, namespace: group, name: 'project A') }
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index a07a8ae4292..90b9606ec7b 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupVariables do
+RSpec.describe API::GroupVariables, feature_category: :pipeline_authoring do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:variable) { create(:ci_group_variable, group: group) }
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index c94bc1e1bac..12a6553f51a 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Groups do
+RSpec.describe API::Groups, feature_category: :subgroups do
include GroupAPIHelpers
include UploadHelpers
include WorkhorseHelpers
diff --git a/spec/requests/api/helm_packages_spec.rb b/spec/requests/api/helm_packages_spec.rb
index 6bd81f64913..584f6e3c7d4 100644
--- a/spec/requests/api/helm_packages_spec.rb
+++ b/spec/requests/api/helm_packages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::HelmPackages do
+RSpec.describe API::HelmPackages, feature_category: :package_registry do
include_context 'helm api setup'
using RSpec::Parameterized::TableSyntax
@@ -17,6 +17,8 @@ RSpec.describe API::HelmPackages do
let_it_be(:package_file2_2) { create(:helm_package_file, package: package2, file_sha256: 'file2', file_name: 'filename2.tgz', channel: 'test', description: 'hello from test channel') }
let_it_be(:other_package) { create(:npm_package, project: project) }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_helm_user' } }
+
describe 'GET /api/v4/projects/:id/packages/helm/:channel/index.yaml' do
let(:project_id) { project.id }
let(:channel) { 'stable' }
@@ -63,7 +65,6 @@ RSpec.describe API::HelmPackages do
with_them do
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
before do
project.update!(visibility: visibility.to_s)
@@ -74,8 +75,6 @@ RSpec.describe API::HelmPackages do
end
context 'with access to package registry for everyone' do
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
-
before do
project.update!(visibility: Gitlab::VisibilityLevel::PRIVATE)
project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
@@ -152,7 +151,6 @@ RSpec.describe API::HelmPackages do
let(:params) { { chart: temp_file(file_name) } }
let(:file_key) { :chart }
let(:send_rewritten_field) { true }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
subject do
workhorse_finalize(
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index e29e5c31a34..38275ce0057 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require 'raven/transports/dummy'
require_relative '../../../config/initializers/sentry'
-RSpec.describe API::Helpers do
+RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :authentication_and_authorization do
include API::APIGuard::HelperMethods
include described_class
include TermsHelper
diff --git a/spec/requests/api/import_bitbucket_server_spec.rb b/spec/requests/api/import_bitbucket_server_spec.rb
index 8ab41f49549..7c2df52fdf3 100644
--- a/spec/requests/api/import_bitbucket_server_spec.rb
+++ b/spec/requests/api/import_bitbucket_server_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ImportBitbucketServer do
+RSpec.describe API::ImportBitbucketServer, feature_category: :importers do
let(:base_uri) { "https://test:7990" }
let(:user) { create(:user) }
let(:token) { "asdasd12345" }
diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb
index 4f95295c14d..dce82f1cf37 100644
--- a/spec/requests/api/import_github_spec.rb
+++ b/spec/requests/api/import_github_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ImportGithub do
+RSpec.describe API::ImportGithub, feature_category: :importers do
let(:token) { "asdasd12345" }
let(:provider) { :github }
let(:access_params) { { github_access_token: token } }
diff --git a/spec/requests/api/integrations/jira_connect/subscriptions_spec.rb b/spec/requests/api/integrations/jira_connect/subscriptions_spec.rb
index 8a222a99b34..8d7b462771d 100644
--- a/spec/requests/api/integrations/jira_connect/subscriptions_spec.rb
+++ b/spec/requests/api/integrations/jira_connect/subscriptions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Integrations::JiraConnect::Subscriptions do
+RSpec.describe API::Integrations::JiraConnect::Subscriptions, feature_category: :integrations do
describe 'POST /integrations/jira_connect/subscriptions' do
subject(:post_subscriptions) { post api('/integrations/jira_connect/subscriptions') }
@@ -20,20 +20,6 @@ RSpec.describe API::Integrations::JiraConnect::Subscriptions do
post api('/integrations/jira_connect/subscriptions', user), params: { jwt: jwt, namespace_path: group.path }
end
- context 'with feature flag disabled' do
- before do
- stub_feature_flags(jira_connect_oauth: false)
- end
-
- let(:jwt) { '123' }
-
- it 'returns 404' do
- post_subscriptions
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
context 'with invalid JWT' do
let(:jwt) { '123' }
diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb
index 1e8061f9606..c35b9bab0ec 100644
--- a/spec/requests/api/integrations_spec.rb
+++ b/spec/requests/api/integrations_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe API::Integrations do
+RSpec.describe API::Integrations, feature_category: :integrations do
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 32cacfc713c..f9284f21aaa 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::Base do
+RSpec.describe API::Internal::Base, feature_category: :authentication_and_authorization do
include GitlabShellHelpers
include APIInternalBaseHelpers
@@ -325,6 +325,28 @@ RSpec.describe API::Internal::Base do
expect(json_response['name']).to eq(user.name)
end
+ context 'when signing key is passed' do
+ it 'does not authenticate user' do
+ key.signing!
+
+ get(api("/internal/discover"), params: { key_id: key.id }, headers: gitlab_shell_internal_api_request_header)
+
+ expect(json_response).to be_nil
+ end
+ end
+
+ context 'when auth-only key is passed' do
+ it 'authenticates user' do
+ key.auth!
+
+ get(api("/internal/discover"), params: { key_id: key.id }, headers: gitlab_shell_internal_api_request_header)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(json_response['name']).to eq(user.name)
+ end
+ end
+
it "finds a user by username" do
get(api("/internal/discover"), params: { username: user.username }, headers: gitlab_shell_internal_api_request_header)
@@ -360,6 +382,30 @@ RSpec.describe API::Internal::Base do
expect(json_response['key'].split[1]).to eq(key.key.split[1])
end
+ context 'when signing key is passed' do
+ it 'does not return the key' do
+ key.signing!
+
+ get(api('/internal/authorized_keys'), params: { key: key.key.split[1] }, headers: gitlab_shell_internal_api_request_header)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+
+ expect(json_response['id']).to be_nil
+ end
+ end
+
+ context 'when auth-only key is passed' do
+ it 'authenticates user' do
+ key.auth!
+
+ get(api('/internal/authorized_keys'), params: { key: key.key.split[1] }, headers: gitlab_shell_internal_api_request_header)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(key.id)
+ expect(json_response['key'].split[1]).to eq(key.key.split[1])
+ end
+ end
+
it 'exposes the comment of the key as a simple identifier of username + hostname' do
get(api('/internal/authorized_keys'), params: { key: key.key.split[1] }, headers: gitlab_shell_internal_api_request_header)
diff --git a/spec/requests/api/internal/container_registry/migration_spec.rb b/spec/requests/api/internal/container_registry/migration_spec.rb
index db2918e65f1..b9258e4627a 100644
--- a/spec/requests/api/internal/container_registry/migration_spec.rb
+++ b/spec/requests/api/internal/container_registry/migration_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::ContainerRegistry::Migration, :aggregate_failures do
+RSpec.describe API::Internal::ContainerRegistry::Migration, :aggregate_failures, feature_category: :database do
let_it_be_with_reload(:repository) { create(:container_repository) }
let(:secret_token) { 'secret_token' }
diff --git a/spec/requests/api/internal/error_tracking_spec.rb b/spec/requests/api/internal/error_tracking_spec.rb
index 4c420eb8505..83012e26138 100644
--- a/spec/requests/api/internal/error_tracking_spec.rb
+++ b/spec/requests/api/internal/error_tracking_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::ErrorTracking do
+RSpec.describe API::Internal::ErrorTracking, feature_category: :error_tracking do
let(:secret_token) { Gitlab::CurrentSettings.error_tracking_access_token }
let(:headers) do
{ ::API::Internal::ErrorTracking::GITLAB_ERROR_TRACKING_TOKEN_HEADER => secret_token }
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index 3c6604cf409..dc631ad7921 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::Kubernetes do
+RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_management do
let(:jwt_auth_headers) do
jwt_token = JWT.encode({ 'iss' => Gitlab::Kas::JWT_ISSUER }, Gitlab::Kas.secret, 'HS256')
@@ -103,15 +103,19 @@ RSpec.describe API::Internal::Kubernetes do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'tracks events' do
+ it 'tracks events and unique events', :aggregate_failures do
+ request_count = 2
counters = { gitops_sync: 10, k8s_api_proxy_request: 5 }
- unique_counters = { agent_users_using_ci_tunnel: [10] }
+ unique_counters = { agent_users_using_ci_tunnel: [10, 999, 777, 10] }
expected_counters = {
- kubernetes_agent_gitops_sync: counters[:gitops_sync],
- kubernetes_agent_k8s_api_proxy_request: counters[:k8s_api_proxy_request]
+ kubernetes_agent_gitops_sync: request_count * counters[:gitops_sync],
+ kubernetes_agent_k8s_api_proxy_request: request_count * counters[:k8s_api_proxy_request]
}
+ expected_hll_count = unique_counters[:agent_users_using_ci_tunnel].uniq.count
- send_request(params: { counters: counters, unique_counters: unique_counters })
+ request_count.times do
+ send_request(params: { counters: counters, unique_counters: unique_counters })
+ end
expect(Gitlab::UsageDataCounters::KubernetesAgentCounter.totals).to eq(expected_counters)
@@ -121,7 +125,7 @@ RSpec.describe API::Internal::Kubernetes do
event_names: 'agent_users_using_ci_tunnel',
start_date: Date.current, end_date: Date.current + 10
)
- ).to eq(1)
+ ).to eq(expected_hll_count)
end
end
end
diff --git a/spec/requests/api/internal/lfs_spec.rb b/spec/requests/api/internal/lfs_spec.rb
index 9eb48db5bd5..1021c03f736 100644
--- a/spec/requests/api/internal/lfs_spec.rb
+++ b/spec/requests/api/internal/lfs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::Lfs do
+RSpec.describe API::Internal::Lfs, feature_category: :source_code_management do
include GitlabShellHelpers
include APIInternalBaseHelpers
diff --git a/spec/requests/api/internal/mail_room_spec.rb b/spec/requests/api/internal/mail_room_spec.rb
index a0a9c1f9cb3..7baa26e3508 100644
--- a/spec/requests/api/internal/mail_room_spec.rb
+++ b/spec/requests/api/internal/mail_room_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::MailRoom do
+RSpec.describe API::Internal::MailRoom, feature_category: :service_desk do
let(:base_configs) do
{
enabled: true,
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index 5b970ca605c..56f1089843b 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::Pages do
+RSpec.describe API::Internal::Pages, feature_category: :pages do
let(:auth_headers) do
jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256')
{ Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token }
diff --git a/spec/requests/api/internal/workhorse_spec.rb b/spec/requests/api/internal/workhorse_spec.rb
index bcf63bf7c2f..99d0ecabbb7 100644
--- a/spec/requests/api/internal/workhorse_spec.rb
+++ b/spec/requests/api/internal/workhorse_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::Workhorse, :allow_forgery_protection do
+RSpec.describe API::Internal::Workhorse, :allow_forgery_protection, feature_category: :not_owned do
include WorkhorseHelpers
context '/authorize_upload' do
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index c07d2e11363..9d3ab269ca1 100644
--- a/spec/requests/api/invitations_spec.rb
+++ b/spec/requests/api/invitations_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Invitations do
+RSpec.describe API::Invitations, feature_category: :users do
let_it_be(:maintainer) { create(:user, username: 'maintainer_user') }
let_it_be(:maintainer2) { create(:user, username: 'user-with-maintainer-role') }
let_it_be(:developer) { create(:user) }
diff --git a/spec/requests/api/issue_links_spec.rb b/spec/requests/api/issue_links_spec.rb
index 98f72f22cdc..93bf17d72d7 100644
--- a/spec/requests/api/issue_links_spec.rb
+++ b/spec/requests/api/issue_links_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::IssueLinks do
+RSpec.describe API::IssueLinks, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
@@ -189,6 +189,8 @@ RSpec.describe API::IssueLinks do
end
context 'when authenticated' do
+ let_it_be(:target_issue) { create(:issue, project: project) }
+
context 'when issue link does not exist' do
it 'returns 404' do
perform_request(non_existing_record_id, user)
@@ -197,8 +199,6 @@ RSpec.describe API::IssueLinks do
end
end
- let_it_be(:target_issue) { create(:issue, project: project) }
-
context 'when issue link does not belong to the specified issue' do
it 'returns 404' do
other_issue = create(:issue, project: project)
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
index 5c06214316b..0641c2135c1 100644
--- a/spec/requests/api/issues/get_group_issues_spec.rb
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Issues do
+RSpec.describe API::Issues, feature_category: :team_planning do
let_it_be(:user2) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:non_member) { create(:user) }
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
index ec6cc060c83..70966d23576 100644
--- a/spec/requests/api/issues/get_project_issues_spec.rb
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Issues do
+RSpec.describe API::Issues, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace) }
let_it_be(:private_mrs_project) do
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index 0e20b2133db..94f0443e14a 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Issues do
+RSpec.describe API::Issues, feature_category: :team_planning do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
index deaf7be96ab..7305da1305a 100644
--- a/spec/requests/api/issues/post_projects_issues_spec.rb
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Issues do
+RSpec.describe API::Issues, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) do
create(:project, :public, creator_id: user.id, namespace: user.namespace)
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
index d6c57b460e0..2d7439d65c1 100644
--- a/spec/requests/api/issues/put_projects_issues_spec.rb
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Issues do
+RSpec.describe API::Issues, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:owner) { create(:owner) }
let(:user2) { create(:user) }
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index 67c3de324dc..d9a0f061156 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Keys do
+RSpec.describe API::Keys, feature_category: :authentication_and_authorization do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:email) { create(:email, user: user) }
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 97ab90c9776..b5d7d564749 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Labels do
+RSpec.describe API::Labels, feature_category: :team_planning do
def put_labels_api(route_type, user, spec_params, request_params = {})
if route_type == :deprecated
put api("/projects/#{project.id}/labels", user),
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index 5d8ed3dd0f5..82b87007a9b 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Lint do
+RSpec.describe API::Lint, feature_category: :pipeline_authoring do
describe 'POST /ci/lint' do
context 'when signup settings are disabled' do
before do
diff --git a/spec/requests/api/markdown_golden_master_spec.rb b/spec/requests/api/markdown_golden_master_spec.rb
index 4fa946de342..1bb5a1d67ae 100644
--- a/spec/requests/api/markdown_golden_master_spec.rb
+++ b/spec/requests/api/markdown_golden_master_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
# See spec/fixtures/markdown/markdown_golden_master_examples.yml for documentation on how this spec works.
-RSpec.describe API::Markdown, 'Golden Master' do
+RSpec.describe API::Markdown, 'Golden Master', feature_category: :team_planning do
markdown_yml_file_path = File.expand_path('../../fixtures/markdown/markdown_golden_master_examples.yml', __dir__)
include_context 'API::Markdown Golden Master shared context', markdown_yml_file_path
end
diff --git a/spec/requests/api/markdown_snapshot_spec.rb b/spec/requests/api/markdown_snapshot_spec.rb
index f2019172a54..866cbcf8ff6 100644
--- a/spec/requests/api/markdown_snapshot_spec.rb
+++ b/spec/requests/api/markdown_snapshot_spec.rb
@@ -4,6 +4,6 @@ require 'spec_helper'
# See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
# for documentation on this spec.
-RSpec.describe API::Markdown, 'Snapshot' do
+RSpec.describe API::Markdown, 'Snapshot', feature_category: :team_planning do
include_context 'with API::Markdown Snapshot shared context'
end
diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb
index 6239ac4e749..db5bbd610fc 100644
--- a/spec/requests/api/markdown_spec.rb
+++ b/spec/requests/api/markdown_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe API::Markdown do
+RSpec.describe API::Markdown, feature_category: :team_planning do
describe "POST /markdown" do
let(:user) {} # No-op. It gets overwritten in the contexts below.
let(:disable_authenticate_markdown_api) { false }
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index ac8c4aacdf2..092eb442f1f 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::MavenPackages do
+RSpec.describe API::MavenPackages, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
include WorkhorseHelpers
@@ -22,7 +22,8 @@ RSpec.describe API::MavenPackages do
let_it_be(:deploy_token_for_group) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token_for_group, group: group) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } }
+
let(:package_name) { 'com/example/my-app' }
let(:headers) { workhorse_headers }
let(:headers_with_token) { headers.merge('Private-Token' => personal_access_token.token) }
@@ -98,8 +99,6 @@ RSpec.describe API::MavenPackages do
context 'with jar file' do
let_it_be(:package_file) { jar_file }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
-
it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
end
end
@@ -900,6 +899,8 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'package workhorse uploads'
context 'event tracking' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_maven_user' } }
+
it_behaves_like 'a package tracking event', described_class.name, 'push_package'
context 'when the package file fails to be created' do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 69be574f38a..4eff5e96e9c 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Members do
+RSpec.describe API::Members, feature_category: :subgroups do
let_it_be(:maintainer) { create(:user, username: 'maintainer_user') }
let_it_be(:maintainer2) { create(:user, username: 'user-with-maintainer-role') }
let_it_be(:developer) { create(:user) }
diff --git a/spec/requests/api/merge_request_approvals_spec.rb b/spec/requests/api/merge_request_approvals_spec.rb
index b18f3017e03..a1d6abec97e 100644
--- a/spec/requests/api/merge_request_approvals_spec.rb
+++ b/spec/requests/api/merge_request_approvals_spec.rb
@@ -2,8 +2,10 @@
require 'spec_helper'
-RSpec.describe API::MergeRequestApprovals do
+RSpec.describe API::MergeRequestApprovals, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ 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 }
@@ -87,4 +89,83 @@ RSpec.describe API::MergeRequestApprovals do
end
end
end
+
+ describe 'PUT :id/merge_requests/:merge_request_iid/reset_approvals' do
+ before do
+ merge_request.approvals.create!(user: user2)
+ create(:project_member, :maintainer, user: bot, source: project)
+ end
+
+ context 'for a bot user' do
+ it 'clears approvals of the merge_request' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot)
+
+ merge_request.reload
+ expect(response).to have_gitlab_http_status(:accepted)
+ expect(merge_request.approvals).to be_empty
+ end
+
+ context 'when bot user approved the merge request' do
+ before do
+ merge_request.approvals.create!(user: bot)
+ end
+
+ it 'clears approvals of the merge_request' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot)
+
+ merge_request.reload
+ expect(response).to have_gitlab_http_status(:accepted)
+ expect(merge_request.approvals).to be_empty
+ end
+ end
+ end
+
+ context 'for users with non-bot roles' do
+ let(:human_user) { create(:user) }
+
+ [:add_owner, :add_maintainer, :add_developer, :add_guest].each do |role_method|
+ it 'returns 401' do
+ project.send(role_method, human_user)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", human_user)
+
+ merge_request.reload
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(merge_request.approvals.pluck(:user_id)).to contain_exactly(user2.id)
+ end
+ end
+ end
+
+ context 'for bot-users from external namespaces' do
+ let_it_be(:external_bot) { create(:user, :project_bot) }
+
+ context 'for external group bot-user' do
+ before do
+ create(:group_member, :maintainer, user: external_bot, source: create(:group))
+ end
+
+ it 'returns 401' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot)
+
+ merge_request.reload
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(merge_request.approvals.pluck(:user_id)).to contain_exactly(user2.id)
+ end
+ end
+
+ context 'for external project bot-user' do
+ before do
+ create(:project_member, :maintainer, user: external_bot, source: create(:project))
+ end
+
+ it 'returns 401' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot)
+
+ merge_request.reload
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(merge_request.approvals.pluck(:user_id)).to contain_exactly(user2.id)
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index caef946273a..4f812e5d8eb 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
+RSpec.describe API::MergeRequestDiffs, 'MergeRequestDiffs', feature_category: :source_code_management do
let!(:user) { create(:user) }
let!(:merge_request) { create(:merge_request, importing: true) }
let!(:project) { merge_request.target_project }
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index eea223485ce..0b69000ae7e 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -2,14 +2,13 @@
require "spec_helper"
-RSpec.describe API::MergeRequests do
+RSpec.describe API::MergeRequests, feature_category: :source_code_management do
include ProjectForksHelper
let_it_be(:base_time) { Time.now }
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
- let_it_be(:bot) { create(:user, :project_bot) }
let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone1) { create(:milestone, title: '0.9', project: project) }
@@ -1788,6 +1787,58 @@ RSpec.describe API::MergeRequests do
end
end
+ describe 'GET /projects/:id/merge_requests/:merge_request_iid/diffs' do
+ let_it_be(:merge_request) do
+ create(
+ :merge_request,
+ :simple,
+ author: user,
+ assignees: [user],
+ source_project: project,
+ target_project: project,
+ source_branch: 'markdown',
+ title: "Test",
+ created_at: base_time
+ )
+ end
+
+ it 'returns a 404 when merge_request_iid not found' do
+ get api("/projects/#{project.id}/merge_requests/0/diffs", user)
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns a 404 when merge_request id is used instead of iid' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/diffs", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when merge request author has only guest access' do
+ it_behaves_like 'rejects user from accessing merge request info' do
+ let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/diffs" }
+ end
+ end
+
+ it 'returns the diffs of the merge_request' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/diffs", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.size).to eq(merge_request.diffs.size)
+ end
+
+ context 'when pagination params are present' do
+ it 'returns limited diffs' do
+ get(
+ api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/diffs", user),
+ params: { page: 1, per_page: 1 }
+ )
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.size).to eq(1)
+ end
+ end
+ end
+
describe 'GET /projects/:id/merge_requests/:merge_request_iid/pipelines' do
let_it_be(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
@@ -3560,71 +3611,6 @@ RSpec.describe API::MergeRequests do
end
end
- describe 'PUT :id/merge_requests/:merge_request_iid/reset_approvals' do
- before do
- merge_request.approvals.create!(user: user2)
- create(:project_member, :maintainer, user: bot, source: project)
- end
-
- context 'when reset_approvals can be performed' do
- it 'clears approvals of the merge_request' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot)
-
- merge_request.reload
- expect(response).to have_gitlab_http_status(:accepted)
- expect(merge_request.approvals).to be_empty
- end
-
- it 'for users with bot role' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot)
-
- expect(response).to have_gitlab_http_status(:accepted)
- end
-
- context 'for users with non-bot roles' do
- let(:human_user) { create(:user) }
-
- [:add_owner, :add_maintainer, :add_developer, :add_guest].each do |role_method|
- it 'returns 401' do
- project.send(role_method, human_user)
-
- put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", human_user)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
- end
-
- context 'for bot-users from external namespaces' do
- let_it_be(:external_bot) { create(:user, :project_bot) }
-
- context 'external group bot-user' do
- before do
- create(:group_member, :maintainer, user: external_bot, source: create(:group))
- end
-
- it 'returns 401' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'external project bot-user' do
- before do
- create(:project_member, :maintainer, user: external_bot, source: create(:project))
- end
-
- it 'returns 401' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
- end
- end
- end
-
describe 'Time tracking' do
let!(:issuable) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
diff --git a/spec/requests/api/metadata_spec.rb b/spec/requests/api/metadata_spec.rb
index 5b6407c689b..b9bdadb01cc 100644
--- a/spec/requests/api/metadata_spec.rb
+++ b/spec/requests/api/metadata_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Metadata do
+RSpec.describe API::Metadata, feature_category: :not_owned do
shared_examples_for 'GET /metadata' do
context 'when unauthenticated' do
it 'returns authentication error' do
diff --git a/spec/requests/api/metrics/dashboard/annotations_spec.rb b/spec/requests/api/metrics/dashboard/annotations_spec.rb
index a09596f167d..7932dd29e4d 100644
--- a/spec/requests/api/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/metrics/dashboard/annotations_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Metrics::Dashboard::Annotations do
+RSpec.describe API::Metrics::Dashboard::Annotations, feature_category: :metrics do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private, :repository, namespace: user.namespace) }
let_it_be(:environment) { create(:environment, project: project) }
diff --git a/spec/requests/api/metrics/user_starred_dashboards_spec.rb b/spec/requests/api/metrics/user_starred_dashboards_spec.rb
index 7f019e1226a..38d3c0be8b2 100644
--- a/spec/requests/api/metrics/user_starred_dashboards_spec.rb
+++ b/spec/requests/api/metrics/user_starred_dashboards_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Metrics::UserStarredDashboards do
+RSpec.describe API::Metrics::UserStarredDashboards, feature_category: :metrics do
let_it_be(:user) { create(:user) }
let_it_be(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
let_it_be(:dashboard) { '.gitlab/dashboards/find&seek.yml' }
diff --git a/spec/requests/api/ml/mlflow_spec.rb b/spec/requests/api/ml/mlflow_spec.rb
index 9448f009742..c1ed7d56ba4 100644
--- a/spec/requests/api/ml/mlflow_spec.rb
+++ b/spec/requests/api/ml/mlflow_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'mime/types'
-RSpec.describe API::Ml::Mlflow do
+RSpec.describe API::Ml::Mlflow, feature_category: :mlops do
include SessionHelpers
include ApiHelpers
include HttpBasicAuthHelpers
@@ -12,12 +12,13 @@ RSpec.describe API::Ml::Mlflow do
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(:experiment) do
- create(:ml_experiments, user: project.creator, project: project)
+ create(:ml_experiments, :with_metadata, project: project)
end
let_it_be(:candidate) do
create(:ml_candidates,
- :with_metrics_and_params, user: experiment.user, start_time: 1234, experiment: experiment)
+ :with_metrics_and_params, :with_metadata,
+ user: experiment.user, start_time: 1234, experiment: experiment)
end
let_it_be(:tokens) do
@@ -151,7 +152,17 @@ RSpec.describe API::Ml::Mlflow do
'experiment_id' => experiment_iid,
'name' => experiment.name,
'lifecycle_stage' => 'active',
- 'artifact_location' => 'not_implemented'
+ 'artifact_location' => 'not_implemented',
+ 'tags' => [
+ {
+ 'key' => experiment.metadata[0].name,
+ 'value' => experiment.metadata[0].value
+ },
+ {
+ 'key' => experiment.metadata[1].name,
+ 'value' => experiment.metadata[1].value
+ }
+ ]
}
})
end
@@ -187,7 +198,17 @@ RSpec.describe API::Ml::Mlflow do
'experiment_id' => experiment.iid.to_s,
'name' => experiment.name,
'lifecycle_stage' => 'active',
- 'artifact_location' => 'not_implemented'
+ 'artifact_location' => 'not_implemented',
+ 'tags' => [
+ {
+ 'key' => experiment.metadata[0].name,
+ 'value' => experiment.metadata[0].value
+ },
+ {
+ 'key' => experiment.metadata[1].name,
+ 'value' => experiment.metadata[1].value
+ }
+ ]
]
})
end
@@ -220,7 +241,17 @@ RSpec.describe API::Ml::Mlflow do
'experiment_id' => experiment.iid.to_s,
'name' => experiment_name,
'lifecycle_stage' => 'active',
- 'artifact_location' => 'not_implemented'
+ 'artifact_location' => 'not_implemented',
+ 'tags' => [
+ {
+ 'key' => experiment.metadata[0].name,
+ 'value' => experiment.metadata[0].value
+ },
+ {
+ 'key' => experiment.metadata[1].name,
+ 'value' => experiment.metadata[1].value
+ }
+ ]
}
})
end
@@ -284,10 +315,44 @@ RSpec.describe API::Ml::Mlflow do
end
end
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/set-experiment-tag' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/set-experiment-tag" }
+ let(:default_params) { { experiment_id: experiment.iid.to_s, key: 'some_key', value: 'value' } }
+ let(:params) { default_params }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'logs the tag', :aggregate_failures do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ expect(experiment.reload.metadata.map(&:name)).to include('some_key')
+ end
+
+ describe 'Error Cases' do
+ context 'when tag was already set' do
+ let(:params) { default_params.merge(key: experiment.metadata[0].name) }
+
+ it_behaves_like 'Bad Request'
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires api scope'
+ it_behaves_like 'Bad Request on missing required', [:key, :value]
+ end
+ end
+
describe 'Runs' do
describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/create' do
let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/create" }
- let(:params) { { experiment_id: experiment.iid.to_s, start_time: Time.now.to_i } }
+ let(:params) do
+ {
+ experiment_id: experiment.iid.to_s,
+ start_time: Time.now.to_i,
+ tags: [
+ { key: 'hello', value: 'world' }
+ ]
+ }
+ end
+
let(:request) { post api(route), params: params, headers: headers }
it 'creates the run', :aggregate_failures do
@@ -295,14 +360,18 @@ RSpec.describe API::Ml::Mlflow do
'experiment_id' => params[:experiment_id],
'user_id' => current_user.id.to_s,
'start_time' => params[:start_time],
- 'status' => "RUNNING",
- 'lifecycle_stage' => "active"
+ 'status' => 'RUNNING',
+ 'lifecycle_stage' => 'active'
}
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('ml/run')
expect(json_response['run']).to include('info' => hash_including(**expected_properties),
- 'data' => { 'metrics' => [], 'params' => [] })
+ 'data' => {
+ 'metrics' => [],
+ 'params' => [],
+ 'tags' => [{ 'key' => 'hello', 'value' => 'world' }]
+ })
end
describe 'Error States' do
@@ -355,6 +424,10 @@ RSpec.describe API::Ml::Mlflow do
'params' => [
{ 'key' => candidate.params[0].name, 'value' => candidate.params[0].value },
{ 'key' => candidate.params[1].name, 'value' => candidate.params[1].value }
+ ],
+ 'tags' => [
+ { 'key' => 'metadata_1', 'value' => 'value1' },
+ { 'key' => 'metadata_2', 'value' => 'value2' }
]
})
end
@@ -454,6 +527,31 @@ RSpec.describe API::Ml::Mlflow do
end
end
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/set-tag' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/set-tag" }
+ let(:default_params) { { run_id: candidate.iid.to_s, key: 'some_key', value: 'value' } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'logs the tag', :aggregate_failures do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ expect(candidate.reload.metadata.map(&:name)).to include('some_key')
+ end
+
+ describe 'Error Cases' do
+ context 'when tag was already logged' do
+ let(:params) { default_params.tap { |p| p[:key] = candidate.metadata[0].name } }
+
+ it_behaves_like 'Bad Request'
+ end
+
+ it_behaves_like 'shared error cases'
+ it_behaves_like 'Requires api scope'
+ it_behaves_like 'run_id param error cases'
+ it_behaves_like 'Bad Request on missing required', [:key, :value]
+ end
+ end
+
describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-batch' do
let(:candidate2) do
create(:ml_candidates, user: experiment.user, start_time: 1234, experiment: experiment)
@@ -467,7 +565,8 @@ RSpec.describe API::Ml::Mlflow do
{ key: 'mae', value: 2.5, timestamp: 1552550804 },
{ key: 'rmse', value: 2.7, timestamp: 1552550804 }
],
- params: [{ key: 'model_class', value: 'LogisticRegression' }]
+ params: [{ key: 'model_class', value: 'LogisticRegression' }],
+ tags: [{ key: 'tag1', value: 'tag.value.1' }]
}
end
@@ -477,6 +576,7 @@ RSpec.describe API::Ml::Mlflow do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_empty
expect(candidate2.params.size).to eq(1)
+ expect(candidate2.metadata.size).to eq(1)
expect(candidate2.metrics.size).to eq(2)
end
@@ -493,6 +593,19 @@ RSpec.describe API::Ml::Mlflow do
end
end
+ context 'when tag was already logged' do
+ let(:params) do
+ default_params.tap { |p| p[:tags] = [{ key: 'tag1', value: 'a' }, { key: 'tag1', value: 'b' }] }
+ end
+
+ it 'logs only 1', :aggregate_failures do
+ candidate.metadata.reload
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(candidate2.metadata.size).to eq(1)
+ end
+ end
+
describe 'Error Cases' do
context 'when required metric key is missing' do
let(:params) { default_params.tap { |p| p[:metrics] = [p[:metrics][0].delete(:key)] } }
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index ab39c29653f..30616964371 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Namespaces do
+RSpec.describe API::Namespaces, feature_category: :subgroups do
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
let_it_be(:group1) { create(:group, name: 'group.one') }
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 89abb28140a..c2d9db1e6fb 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Notes do
+RSpec.describe API::Notes, feature_category: :team_planning do
let!(:user) { create(:user) }
let!(:project) { create(:project, :public) }
let(:private_user) { create(:user) }
@@ -203,6 +203,51 @@ RSpec.describe API::Notes do
end
end
end
+
+ context 'without notes widget' do
+ let(:request_body) { 'Hi!' }
+ let(:params) { { body: request_body } }
+ let(:request_path) { "/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes" }
+
+ before do
+ stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
+ stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+ end
+
+ it 'does not fetch notes' do
+ get api(request_path, private_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'does not fetch specific note' do
+ get api("#{request_path}/#{cross_reference_note.id}", private_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'does not create note' do
+ post api(request_path, private_user), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'does not update note' do
+ put api("#{request_path}/#{cross_reference_note.id}", private_user), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'does not run quick actions' do
+ params[:body] = "/spend 1h"
+
+ expect do
+ post api("#{request_path}/#{cross_reference_note.id}", private_user), params: params
+ end.to not_change { Note.system.count }.and(not_change { Note.where(system: false).count })
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
end
diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb
index b5551c21738..2a80dc4bbe9 100644
--- a/spec/requests/api/notification_settings_spec.rb
+++ b/spec/requests/api/notification_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::NotificationSettings do
+RSpec.describe API::NotificationSettings, feature_category: :team_planning do
let(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:project) { create(:project, :public, creator_id: user.id, namespace: group) }
diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb
index 698885ddcf4..dcd2e4ae677 100644
--- a/spec/requests/api/npm_instance_packages_spec.rb
+++ b/spec/requests/api/npm_instance_packages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::NpmInstancePackages do
+RSpec.describe API::NpmInstancePackages, feature_category: :package_registry do
# We need to create a subgroup with the same name as the hosting group.
# It has to be created first to exhibit this bug: https://gitlab.com/gitlab-org/gitlab/-/issues/321958
let_it_be(:another_namespace) { create(:group, :public) }
@@ -33,4 +33,16 @@ RSpec.describe API::NpmInstancePackages do
let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
end
+
+ describe 'POST /api/v4/packages/npm/-/npm/v1/security/advisories/bulk' do
+ it_behaves_like 'handling audit request', path: 'advisories/bulk', scope: :instance do
+ let(:url) { api('/packages/npm/-/npm/v1/security/advisories/bulk') }
+ end
+ end
+
+ describe 'POST /api/v4/packages/npm/-/npm/v1/security/audits/quick' do
+ it_behaves_like 'handling audit request', path: 'audits/quick', scope: :instance do
+ let(:url) { api('/packages/npm/-/npm/v1/security/audits/quick') }
+ end
+ end
end
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 373327787a2..c62c0849776 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::NpmProjectPackages do
+RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
include_context 'npm api setup'
shared_examples 'accept get request on private project with access to package registry for everyone' do
@@ -42,8 +42,19 @@ RSpec.describe API::NpmProjectPackages do
end
end
+ describe 'POST /api/v4/projects/:id/packages/npm/-/npm/v1/security/advisories/bulk' do
+ it_behaves_like 'handling audit request', path: 'advisories/bulk', scope: :project do
+ let(:url) { api("/projects/#{project.id}/packages/npm/-/npm/v1/security/advisories/bulk") }
+ end
+ end
+
+ describe 'POST /api/v4/projects/:id/packages/npm/-/npm/v1/security/audits/quick' do
+ it_behaves_like 'handling audit request', path: 'audits/quick', scope: :project do
+ let(:url) { api("/projects/#{project.id}/packages/npm/-/npm/v1/security/audits/quick") }
+ end
+ end
+
describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
let(:package_file) { package.package_files.first }
let(:headers) { {} }
@@ -203,7 +214,7 @@ RSpec.describe API::NpmProjectPackages do
let_it_be(:version) { '1.2.3' }
let(:params) { upload_params(package_name: package_name, package_version: version) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_npm_user' } }
shared_examples 'handling upload with different authentications' do
context 'with access token' do
diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb
index c1375288809..9de612f7bc7 100644
--- a/spec/requests/api/nuget_group_packages_spec.rb
+++ b/spec/requests/api/nuget_group_packages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::NugetGroupPackages do
+RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do
include_context 'nuget api setup'
using RSpec::Parameterized::TableSyntax
@@ -12,6 +12,7 @@ RSpec.describe API::NugetGroupPackages do
let_it_be(:deploy_token) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token, group: group) }
+ let(:snowplow_gitlab_standard_context) { { namespace: project.group, property: 'i_package_nuget_user' } }
let(:target_type) { 'groups' }
shared_examples 'handling all endpoints' do
@@ -46,7 +47,6 @@ RSpec.describe API::NugetGroupPackages do
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token, group: subgroup) }
let(:target) { subgroup }
- let(:snowplow_gitlab_standard_context) { { namespace: subgroup } }
it_behaves_like 'handling all endpoints'
@@ -58,7 +58,7 @@ RSpec.describe API::NugetGroupPackages do
context 'a group' do
let(:target) { group }
- let(:snowplow_gitlab_standard_context) { { namespace: group } }
+ let(:snowplow_gitlab_standard_context) { { namespace: target, property: 'i_package_nuget_user' } }
it_behaves_like 'handling all endpoints'
diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb
index f608f296295..1e0d35ad451 100644
--- a/spec/requests/api/nuget_project_packages_spec.rb
+++ b/spec/requests/api/nuget_project_packages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::NugetProjectPackages do
+RSpec.describe API::NugetProjectPackages, feature_category: :package_registry do
include_context 'nuget api setup'
using RSpec::Parameterized::TableSyntax
@@ -9,38 +9,62 @@ RSpec.describe API::NugetProjectPackages do
let_it_be_with_reload(:project) { create(:project, :public) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
+ let_it_be(:package_name) { 'Dummy.Package' }
let(:target) { project }
let(:target_type) { 'projects' }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_nuget_user' } }
- describe 'GET /api/v4/projects/:id/packages/nuget' do
- it_behaves_like 'handling nuget service requests' do
- let(:url) { "/projects/#{target.id}/packages/nuget/index.json" }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
+ shared_examples 'accept get request on private project with access to package registry for everyone' do
+ subject { get api(url) }
+
+ before do
+ update_visibility_to(Gitlab::VisibilityLevel::PRIVATE)
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
end
+
+ it_behaves_like 'returning response status', :ok
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/nuget' do
+ let(:url) { "/projects/#{target.id}/packages/nuget/index.json" }
+
+ it_behaves_like 'handling nuget service requests'
+
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/index' do
- it_behaves_like 'handling nuget metadata requests with package name' do
- let(:url) { "/projects/#{target.id}/packages/nuget/metadata/#{package_name}/index.json" }
+ let(:url) { "/projects/#{target.id}/packages/nuget/metadata/#{package_name}/index.json" }
+
+ it_behaves_like 'handling nuget metadata requests with package name'
+
+ it_behaves_like 'accept get request on private project with access to package registry for everyone' do
+ let_it_be(:packages) { create_list(:nuget_package, 5, :with_metadatum, name: package_name, project: project) }
end
end
describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/*package_version' do
- it_behaves_like 'handling nuget metadata requests with package name and package version' do
- let(:url) { "/projects/#{target.id}/packages/nuget/metadata/#{package_name}/#{package.version}.json" }
+ let(:url) { "/projects/#{target.id}/packages/nuget/metadata/#{package_name}/#{package.version}.json" }
+
+ it_behaves_like 'handling nuget metadata requests with package name and package version'
+
+ it_behaves_like 'accept get request on private project with access to package registry for everyone' do
+ let_it_be(:package) { create(:nuget_package, :with_metadatum, name: package_name, project: project) }
end
end
describe 'GET /api/v4/projects/:id/packages/nuget/query' do
- it_behaves_like 'handling nuget search requests' do
- let(:url) { "/projects/#{target.id}/packages/nuget/query?#{query_parameters.to_query}" }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
+ let(:url) { "/projects/#{target.id}/packages/nuget/query?#{query_parameters.to_query}" }
+
+ it_behaves_like 'handling nuget search requests'
+
+ it_behaves_like 'accept get request on private project with access to package registry for everyone' do
+ let_it_be(:query_parameters) { { q: 'query', take: 5, skip: 0, prerelease: true } }
end
end
describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/index' do
- let_it_be(:package_name) { 'Dummy.Package' }
let_it_be(:packages) { create_list(:nuget_package, 5, name: package_name, project: project) }
let(:url) { "/projects/#{target.id}/packages/nuget/download/#{package_name}/index.json" }
@@ -88,10 +112,11 @@ RSpec.describe API::NugetProjectPackages do
it_behaves_like 'rejects nuget access with unknown target id'
it_behaves_like 'rejects nuget access with invalid target id'
+
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/*package_version/*package_filename' do
- let_it_be(:package_name) { 'Dummy.Package' }
let_it_be(:package) { create(:nuget_package, :with_symbol_package, project: project, name: package_name) }
let(:format) { 'nupkg' }
@@ -124,7 +149,6 @@ RSpec.describe API::NugetProjectPackages do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
subject { get api(url), headers: headers }
@@ -134,6 +158,8 @@ RSpec.describe API::NugetProjectPackages do
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
+
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
it_behaves_like 'deploy token for package GET requests' do
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
index f07dcfcccd6..b29f1e9e661 100644
--- a/spec/requests/api/oauth_tokens_spec.rb
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OAuth tokens' do
+RSpec.describe 'OAuth tokens', feature_category: :authentication_and_authorization do
include HttpBasicAuthHelpers
context 'Resource Owner Password Credentials' do
@@ -85,8 +85,6 @@ RSpec.describe 'OAuth tokens' do
context 'with invalid credentials' do
it 'does not create an access token' do
- pending 'Enable this example after https://github.com/doorkeeper-gem/doorkeeper/pull/1488 is merged and released'
-
user = create(:user)
request_oauth_token(user, basic_auth_header(client.uid, 'invalid secret'))
diff --git a/spec/requests/api/package_files_spec.rb b/spec/requests/api/package_files_spec.rb
index 01c7ef1476f..f47dca387ef 100644
--- a/spec/requests/api/package_files_spec.rb
+++ b/spec/requests/api/package_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::PackageFiles do
+RSpec.describe API::PackageFiles, feature_category: :package_registry do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:package) { create(:maven_package, project: project) }
diff --git a/spec/requests/api/pages/internal_access_spec.rb b/spec/requests/api/pages/internal_access_spec.rb
index 4ac47f17b7e..fdc25ecdcd3 100644
--- a/spec/requests/api/pages/internal_access_spec.rb
+++ b/spec/requests/api/pages/internal_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Internal Project Pages Access" do
+RSpec.describe "Internal Project Pages Access", feature_category: :pages do
using RSpec::Parameterized::TableSyntax
include AccessMatchers
diff --git a/spec/requests/api/pages/pages_spec.rb b/spec/requests/api/pages/pages_spec.rb
index 4a94bf90205..c426f2a433c 100644
--- a/spec/requests/api/pages/pages_spec.rb
+++ b/spec/requests/api/pages/pages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Pages do
+RSpec.describe API::Pages, feature_category: :pages do
let_it_be(:project) { create(:project, path: 'my.project', pages_https_only: false) }
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/api/pages/private_access_spec.rb b/spec/requests/api/pages/private_access_spec.rb
index c1c0e406508..5cc1b8f9a69 100644
--- a/spec/requests/api/pages/private_access_spec.rb
+++ b/spec/requests/api/pages/private_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Private Project Pages Access" do
+RSpec.describe "Private Project Pages Access", feature_category: :pages do
using RSpec::Parameterized::TableSyntax
include AccessMatchers
diff --git a/spec/requests/api/pages/public_access_spec.rb b/spec/requests/api/pages/public_access_spec.rb
index c45b3a4c55e..1137f91f4b0 100644
--- a/spec/requests/api/pages/public_access_spec.rb
+++ b/spec/requests/api/pages/public_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Public Project Pages Access" do
+RSpec.describe "Public Project Pages Access", feature_category: :pages do
using RSpec::Parameterized::TableSyntax
include AccessMatchers
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index 8ef4e899193..65fcf9e006a 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::PagesDomains do
+RSpec.describe API::PagesDomains, feature_category: :pages do
let_it_be(:project) { create(:project, path: 'my.project', pages_https_only: false) }
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
diff --git a/spec/requests/api/performance_bar_spec.rb b/spec/requests/api/performance_bar_spec.rb
index a4dbb3d17b8..9fbe34914c5 100644
--- a/spec/requests/api/performance_bar_spec.rb
+++ b/spec/requests/api/performance_bar_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Performance Bar for API requests', :request_store, :clean_gitlab_redis_cache do
+RSpec.describe 'Performance Bar for API requests', :request_store, :clean_gitlab_redis_cache,
+feature_category: :metrics do
context 'with user that has access to the performance bar' do
let_it_be(:admin) { create(:admin) }
diff --git a/spec/requests/api/personal_access_tokens/self_information_spec.rb b/spec/requests/api/personal_access_tokens/self_information_spec.rb
index bdfac3ed14f..4a3c0ad8904 100644
--- a/spec/requests/api/personal_access_tokens/self_information_spec.rb
+++ b/spec/requests/api/personal_access_tokens/self_information_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::PersonalAccessTokens::SelfInformation do
+RSpec.describe API::PersonalAccessTokens::SelfInformation, feature_category: :authentication_and_authorization do
let(:path) { '/personal_access_tokens/self' }
let(:token) { create(:personal_access_token, user: current_user) }
diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb
index 1fa2ad6ebfa..32adc7ebd61 100644
--- a/spec/requests/api/personal_access_tokens_spec.rb
+++ b/spec/requests/api/personal_access_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::PersonalAccessTokens do
+RSpec.describe API::PersonalAccessTokens, feature_category: :authentication_and_authorization do
let_it_be(:path) { '/personal_access_tokens' }
describe 'GET /personal_access_tokens' do
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index 2ff4cd72f1e..cc399d25429 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -43,6 +43,7 @@ itself: # project
- storage_version
- topic_list
- updated_at
+ - mirror_branch_regex
remapped_attributes:
avatar: avatar_url
build_allow_git_fetch: build_git_strategy
@@ -124,10 +125,6 @@ project_feature:
- created_at
- metrics_dashboard_access_level
- package_registry_access_level
- - monitor_access_level
- - infrastructure_access_level
- - feature_flags_access_level
- - environments_access_level
- project_id
- updated_at
computed_attributes:
@@ -163,6 +160,7 @@ project_setting:
- suggested_reviewers_enabled
- jitsu_key
- mirror_branch_regex
+ - allow_pipeline_trigger_approve_deployment
build_service_desk_setting: # service_desk_setting
unexposed_attributes:
diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb
index 4c7da78f0d4..895192252da 100644
--- a/spec/requests/api/project_clusters_spec.rb
+++ b/spec/requests/api/project_clusters_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectClusters do
+RSpec.describe API::ProjectClusters, feature_category: :kubernetes_management do
include KubernetesHelpers
let_it_be(:maintainer_user) { create(:user) }
diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb
index 52ec06d76a9..a2e1a1c1721 100644
--- a/spec/requests/api/project_container_repositories_spec.rb
+++ b/spec/requests/api/project_container_repositories_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectContainerRepositories do
+RSpec.describe API::ProjectContainerRepositories, feature_category: :package_registry do
include ExclusiveLeaseHelpers
let_it_be(:project) { create(:project, :private) }
@@ -33,7 +33,10 @@ RSpec.describe API::ProjectContainerRepositories do
let(:method) { :get }
let(:params) { {} }
- let(:snowplow_gitlab_standard_context) { { user: api_user, project: project, namespace: project.namespace } }
+ let(:snowplow_gitlab_standard_context) do
+ { user: api_user, project: project, namespace: project.namespace,
+ property: 'i_package_container_user' }
+ end
before_all do
project.add_maintainer(maintainer)
@@ -144,20 +147,6 @@ RSpec.describe API::ProjectContainerRepositories do
expect(response).to have_gitlab_http_status(:accepted)
end
-
- context 'with container_registry_delete_repository_with_cron_worker disabled' do
- before do
- stub_feature_flags(container_registry_delete_repository_with_cron_worker: false)
- end
-
- it 'schedules removal of repository' do
- expect(DeleteContainerRepositoryWorker).to receive(:perform_async)
- .with(maintainer.id, root_repository.id)
- expect { subject }.to change { root_repository.reload.status }.from(nil).to('delete_scheduled')
-
- expect(response).to have_gitlab_http_status(:accepted)
- end
- end
end
end
end
@@ -414,6 +403,9 @@ RSpec.describe API::ProjectContainerRepositories do
context 'for developer', :snowplow do
let(:api_user) { developer }
+ let(:service_ping_context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'i_package_container_user').to_h]
+ end
context 'when there are multiple tags' do
before do
@@ -427,7 +419,10 @@ RSpec.describe API::ProjectContainerRepositories do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect_snowplow_event(category: described_class.name, action: 'delete_tag', project: project, user: api_user, namespace: project.namespace)
+ expect_snowplow_event(category: described_class.name, action: 'delete_tag', project: project,
+ user: api_user, namespace: project.namespace.reload,
+ label: 'redis_hll_counters.user_packages.user_packages_total_unique_counts_monthly',
+ property: 'i_package_container_user', context: service_ping_context)
end
end
@@ -443,7 +438,10 @@ RSpec.describe API::ProjectContainerRepositories do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect_snowplow_event(category: described_class.name, action: 'delete_tag', project: project, user: api_user, namespace: project.namespace)
+ expect_snowplow_event(category: described_class.name, action: 'delete_tag', project: project,
+ user: api_user, namespace: project.namespace.reload,
+ label: 'redis_hll_counters.user_packages.user_packages_total_unique_counts_monthly',
+ property: 'i_package_container_user', context: service_ping_context)
end
end
end
diff --git a/spec/requests/api/project_debian_distributions_spec.rb b/spec/requests/api/project_debian_distributions_spec.rb
index 2b993f24046..9807f177c5d 100644
--- a/spec/requests/api/project_debian_distributions_spec.rb
+++ b/spec/requests/api/project_debian_distributions_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::ProjectDebianDistributions do
+RSpec.describe API::ProjectDebianDistributions, feature_category: :package_registry do
include HttpBasicAuthHelpers
include WorkhorseHelpers
@@ -23,13 +23,13 @@ RSpec.describe API::ProjectDebianDistributions do
describe 'GET projects/:id/debian_distributions' do
let(:url) { "/projects/#{container.id}/debian_distributions" }
- it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^\[{.*"codename":"existing-codename\",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/
+ it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^\[{.*"codename":"existing-codename",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/
end
describe 'GET projects/:id/debian_distributions/:codename' do
let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}" }
- it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^{.*"codename":"existing-codename\",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/
+ it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^{.*"codename":"existing-codename",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/
end
describe 'GET projects/:id/debian_distributions/:codename/key.asc' do
@@ -56,7 +56,7 @@ RSpec.describe API::ProjectDebianDistributions do
let(:method) { :delete }
let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}" }
- it_behaves_like 'Debian distributions maintainer write endpoint', 'DELETE', :success, /^{\"message\":\"202 Accepted\"}$/
+ it_behaves_like 'Debian distributions maintainer write endpoint', 'DELETE', :success, /^{"message":"202 Accepted"}$/
context 'when destroy fails' do
before do
diff --git a/spec/requests/api/project_events_spec.rb b/spec/requests/api/project_events_spec.rb
index f3e592f9796..69d8eb76cf3 100644
--- a/spec/requests/api/project_events_spec.rb
+++ b/spec/requests/api/project_events_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectEvents do
+RSpec.describe API::ProjectEvents, feature_category: :users do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index d74fd82ca09..fdd76c63069 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do
+RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:project_none) { create(:project) }
let_it_be(:project_started) { create(:project) }
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 2d925620a91..8e5e9d847ea 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectHooks, 'ProjectHooks' do
+RSpec.describe API::ProjectHooks, 'ProjectHooks', feature_category: :integrations do
let_it_be(:user) { create(:user) }
let_it_be(:user3) { create(:user) }
let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index 05fe55b06a1..027c61bb9e1 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectImport, :aggregate_failures do
+RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :importers do
include WorkhorseHelpers
include AfterNextHelpers
@@ -44,7 +44,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures do
it_behaves_like 'requires authentication'
- it 'executes a limited number of queries' do
+ it 'executes a limited number of queries', :use_clean_rails_redis_caching do
control_count = ActiveRecord::QueryRecorder.new { subject }.count
expect(control_count).to be <= 111
@@ -126,13 +126,31 @@ RSpec.describe API::ProjectImport, :aggregate_failures do
end
end
- it 'schedules an import at the user namespace level' do
- stub_import(user.namespace)
- params[:path] = 'test-import2'
+ context 'when namespace not set' do
+ it 'schedules an import at the user namespace level' do
+ stub_import(user.namespace)
+ params[:path] = 'test-import2'
- subject
+ subject
- expect(response).to have_gitlab_http_status(:created)
+ expect(response).to have_gitlab_http_status(:created)
+ end
+
+ context 'when current user is a bot user' do
+ let(:user) { create(:user, :project_bot) }
+
+ it 'does not schedule an import' do
+ expect_any_instance_of(ProjectImportState).not_to receive(:schedule)
+
+ params[:namespace] = nil
+ params[:path] = 'test-import3'
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq("Namespace is not valid")
+ end
+ end
end
it 'does not schedule an import for a namespace that does not exist' do
@@ -161,6 +179,20 @@ RSpec.describe API::ProjectImport, :aggregate_failures do
expect(json_response['message']).to eq('404 Namespace Not Found')
end
+ context 'when passed in namespace is a bot user namespace' do
+ let(:user) { create(:user, :project_bot) }
+
+ it 'does not schedule an import' do
+ expect_any_instance_of(ProjectImportState).not_to receive(:schedule)
+ params[:namespace] = user.namespace.full_path
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq("Namespace is not valid")
+ end
+ end
+
context 'if user uploads no valid file' do
let(:file) { 'README.md' }
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
index 8294ca143d3..9d722e4a445 100644
--- a/spec/requests/api/project_milestones_spec.rb
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectMilestones do
+RSpec.describe API::ProjectMilestones, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, namespace: user.namespace) }
let_it_be(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb
index 00d295b3490..d3adef85f8d 100644
--- a/spec/requests/api/project_packages_spec.rb
+++ b/spec/requests/api/project_packages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectPackages do
+RSpec.describe API::ProjectPackages, feature_category: :package_registry do
let_it_be(:project) { create(:project, :public) }
let(:user) { create(:user) }
@@ -350,6 +350,16 @@ RSpec.describe API::ProjectPackages do
end
end
end
+
+ context 'when package has no default status' do
+ let!(:package1) { create(:npm_package, :error, project: project) }
+
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
describe 'DELETE /projects/:id/packages/:package_id' do
diff --git a/spec/requests/api/project_repository_storage_moves_spec.rb b/spec/requests/api/project_repository_storage_moves_spec.rb
index 5b272121233..96ed3042d00 100644
--- a/spec/requests/api/project_repository_storage_moves_spec.rb
+++ b/spec/requests/api/project_repository_storage_moves_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectRepositoryStorageMoves do
+RSpec.describe API::ProjectRepositoryStorageMoves, feature_category: :gitaly do
it_behaves_like 'repository_storage_moves API', 'projects' do
let_it_be(:container) { create(:project, :repository) }
let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) }
diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb
index bf78ff56206..5d3c596e605 100644
--- a/spec/requests/api/project_snapshots_spec.rb
+++ b/spec/requests/api/project_snapshots_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectSnapshots do
+RSpec.describe API::ProjectSnapshots, feature_category: :source_code_management do
include WorkhorseHelpers
let(:project) { create(:project) }
@@ -21,7 +21,7 @@ RSpec.describe API::ProjectSnapshots do
expect(type).to eq('git-snapshot')
expect(params).to eq(
'GitalyServer' => {
- 'features' => { 'gitaly-feature-foobar' => 'true' },
+ 'call_metadata' => { 'gitaly-feature-foobar' => 'true' },
'address' => Gitlab::GitalyClient.address(repository.project.repository_storage),
'token' => Gitlab::GitalyClient.token(repository.project.repository_storage)
},
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 1d255f7c1d8..568486deb7f 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectSnippets do
+RSpec.describe API::ProjectSnippets, feature_category: :source_code_management do
include SnippetHelpers
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/requests/api/project_statistics_spec.rb b/spec/requests/api/project_statistics_spec.rb
index d314af0746a..39ead8cc573 100644
--- a/spec/requests/api/project_statistics_spec.rb
+++ b/spec/requests/api/project_statistics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectStatistics do
+RSpec.describe API::ProjectStatistics, feature_category: :source_code_management do
let_it_be(:reporter) { create(:user) }
let_it_be(:public_project) { create(:project, :public) }
diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb
index 87d70a87f42..38d6a05a104 100644
--- a/spec/requests/api/project_templates_spec.rb
+++ b/spec/requests/api/project_templates_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectTemplates do
+RSpec.describe API::ProjectTemplates, feature_category: :source_code_management do
let_it_be(:public_project) { create(:project, :public, :repository, create_templates: :merge_request, path: 'path.with.dot') }
let_it_be(:private_project) { create(:project, :private, :repository, create_templates: :issue) }
let_it_be(:developer) { create(:user) }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 3831e8e1dfe..6e8168c0ee1 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.shared_examples 'languages and percentages JSON response' do
+RSpec.shared_examples 'languages and percentages JSON response', feature_category: :projects do
let(:expected_languages) { project.repository.languages.to_h { |language| language.values_at(:label, :value) } }
before do
@@ -231,14 +231,16 @@ RSpec.describe API::Projects do
include_examples 'includes container_registry_access_level'
end
- it 'includes releases_access_level', :aggregate_failures do
- project.project_feature.update!(releases_access_level: ProjectFeature::DISABLED)
-
+ it 'includes various project feature fields', :aggregate_failures do
get api('/projects', user)
project_response = json_response.find { |p| p['id'] == project.id }
expect(response).to have_gitlab_http_status(:ok)
- expect(project_response['releases_access_level']).to eq('disabled')
+ expect(project_response['releases_access_level']).to eq('enabled')
+ expect(project_response['environments_access_level']).to eq('enabled')
+ expect(project_response['feature_flags_access_level']).to eq('enabled')
+ expect(project_response['infrastructure_access_level']).to eq('enabled')
+ expect(project_response['monitor_access_level']).to eq('enabled')
end
context 'when some projects are in a group' do
@@ -1192,6 +1194,10 @@ RSpec.describe API::Projects do
attrs[:container_registry_access_level] = 'private'
attrs[:security_and_compliance_access_level] = 'private'
attrs[:releases_access_level] = 'disabled'
+ attrs[:environments_access_level] = 'disabled'
+ attrs[:feature_flags_access_level] = 'disabled'
+ attrs[:infrastructure_access_level] = 'disabled'
+ attrs[:monitor_access_level] = 'disabled'
end
post api('/projects', user), params: project
@@ -1201,7 +1207,8 @@ RSpec.describe API::Projects do
project.each_pair do |k, v|
next if %i[
has_external_issue_tracker has_external_wiki issues_enabled merge_requests_enabled wiki_enabled storage_version
- container_registry_access_level releases_access_level
+ container_registry_access_level releases_access_level environments_access_level feature_flags_access_level
+ infrastructure_access_level monitor_access_level
].include?(k)
expect(json_response[k.to_s]).to eq(v)
@@ -1217,6 +1224,10 @@ RSpec.describe API::Projects do
expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::PRIVATE)
expect(project.project_feature.security_and_compliance_access_level).to eq(ProjectFeature::PRIVATE)
expect(project.project_feature.releases_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.environments_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.feature_flags_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.infrastructure_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.monitor_access_level).to eq(ProjectFeature::DISABLED)
end
it 'assigns container_registry_enabled to project', :aggregate_failures do
@@ -2356,6 +2367,10 @@ RSpec.describe API::Projects do
expect(json_response['operations_access_level']).to be_present
expect(json_response['security_and_compliance_access_level']).to be_present
expect(json_response['releases_access_level']).to be_present
+ expect(json_response['environments_access_level']).to be_present
+ expect(json_response['feature_flags_access_level']).to be_present
+ expect(json_response['infrastructure_access_level']).to be_present
+ expect(json_response['monitor_access_level']).to be_present
end
it 'exposes all necessary attributes' do
@@ -2426,6 +2441,10 @@ RSpec.describe API::Projects do
expect(json_response['operations_access_level']).to be_present
expect(json_response['security_and_compliance_access_level']).to be_present
expect(json_response['releases_access_level']).to be_present
+ expect(json_response['environments_access_level']).to be_present
+ expect(json_response['feature_flags_access_level']).to be_present
+ expect(json_response['infrastructure_access_level']).to be_present
+ expect(json_response['monitor_access_level']).to be_present
expect(json_response).to have_key('emails_disabled')
expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions)
expect(json_response['remove_source_branch_after_merge']).to be_truthy
@@ -3410,12 +3429,14 @@ RSpec.describe API::Projects do
expect(Project.find_by(path: project[:path]).analytics_access_level).to eq(ProjectFeature::PRIVATE)
end
- it 'sets releases_access_level', :aggregate_failures do
- put api("/projects/#{project.id}", user), params: { releases_access_level: 'private' }
+ %i(releases_access_level environments_access_level feature_flags_access_level infrastructure_access_level monitor_access_level).each do |field|
+ it "sets #{field}", :aggregate_failures do
+ put api("/projects/#{project.id}", user), params: { field => 'private' }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['releases_access_level']).to eq('private')
- expect(Project.find_by(path: project[:path]).releases_access_level).to eq(ProjectFeature::PRIVATE)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response[field.to_s]).to eq('private')
+ expect(Project.find_by(path: project[:path]).public_send(field)).to eq(ProjectFeature::PRIVATE)
+ end
end
it 'returns 400 when nothing sent' do
@@ -4687,6 +4708,7 @@ RSpec.describe API::Projects do
end
end
end
+
describe 'PUT /projects/:id/transfer' do
context 'when authenticated as owner' do
let(:group) { create :group }
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index b46859a0e70..8e8a25a8dc2 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProtectedBranches do
+RSpec.describe API::ProtectedBranches, feature_category: :source_code_management do
let_it_be_with_reload(:project) { create(:project, :repository) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:guest) { create(:user) }
diff --git a/spec/requests/api/protected_tags_spec.rb b/spec/requests/api/protected_tags_spec.rb
index f1db39ac204..5b128d4ec9e 100644
--- a/spec/requests/api/protected_tags_spec.rb
+++ b/spec/requests/api/protected_tags_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProtectedTags do
+RSpec.describe API::ProtectedTags, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:project2) { create(:project, path: 'project2', namespace: user.namespace) }
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index 12091158a02..59d93cd48e3 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::PypiPackages do
+RSpec.describe API::PypiPackages, feature_category: :package_registry do
include WorkhorseHelpers
include PackagesManagerApiSpecHelpers
include HttpBasicAuthHelpers
@@ -14,6 +14,7 @@ RSpec.describe API::PypiPackages do
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } }
let(:headers) { {} }
@@ -25,7 +26,7 @@ RSpec.describe API::PypiPackages do
describe 'GET /api/v4/groups/:id/-/packages/pypi/simple' do
let(:url) { "/groups/#{group.id}/-/packages/pypi/simple" }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } }
it_behaves_like 'pypi simple index API endpoint'
it_behaves_like 'rejects PyPI access with unknown group id'
@@ -63,7 +64,7 @@ RSpec.describe API::PypiPackages do
describe 'GET /api/v4/projects/:id/packages/pypi/simple' do
let(:package_name) { package.name }
let(:url) { "/projects/#{project.id}/packages/pypi/simple" }
- let(:snowplow_gitlab_standard_context) { { project: nil, namespace: group } }
+ let(:snowplow_gitlab_standard_context) { { project: nil, namespace: group, property: 'i_package_pypi_user' } }
it_behaves_like 'pypi simple index API endpoint'
it_behaves_like 'rejects PyPI access with unknown project id'
@@ -81,13 +82,13 @@ RSpec.describe API::PypiPackages do
context 'simple package API endpoint' do
let_it_be(:package) { create(:pypi_package, project: project) }
+ let(:snowplow_gitlab_standard_context) { { project: nil, namespace: group, property: 'i_package_pypi_user' } }
subject { get api(url), headers: headers }
describe 'GET /api/v4/groups/:id/-/packages/pypi/simple/:package_name' do
let(:package_name) { package.name }
let(:url) { "/groups/#{group.id}/-/packages/pypi/simple/#{package_name}" }
- let(:snowplow_gitlab_standard_context) { { project: nil, namespace: group } }
it_behaves_like 'pypi simple API endpoint'
it_behaves_like 'rejects PyPI access with unknown group id'
@@ -125,7 +126,7 @@ RSpec.describe API::PypiPackages do
describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do
let(:package_name) { package.name }
let(:url) { "/projects/#{project.id}/packages/pypi/simple/#{package_name}" }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } }
it_behaves_like 'pypi simple API endpoint'
it_behaves_like 'rejects PyPI access with unknown project id'
@@ -202,7 +203,7 @@ RSpec.describe API::PypiPackages do
let(:base_params) { { requires_python: requires_python, version: '1.0.0', name: 'sample-project', sha256_digest: '1' * 64, md5_digest: '1' * 32 } }
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 } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_pypi_user' } }
subject do
workhorse_finalize(
@@ -366,7 +367,6 @@ RSpec.describe API::PypiPackages do
describe 'GET /api/v4/groups/:id/-/packages/pypi/files/:sha256/*file_identifier' do
let(:url) { "/groups/#{group.id}/-/packages/pypi/files/#{package.package_files.first.file_sha256}/#{package_name}-1.0.0.tar.gz" }
- let(:snowplow_gitlab_standard_context) { {} }
it_behaves_like 'pypi file download endpoint'
it_behaves_like 'rejects PyPI access with unknown group id'
@@ -375,7 +375,6 @@ RSpec.describe API::PypiPackages do
describe 'GET /api/v4/projects/:id/packages/pypi/files/:sha256/*file_identifier' do
let(:url) { "/projects/#{project.id}/packages/pypi/files/#{package.package_files.first.file_sha256}/#{package_name}-1.0.0.tar.gz" }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
it_behaves_like 'pypi file download endpoint'
it_behaves_like 'rejects PyPI access with unknown project id'
diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb
index 38166c5ce97..6036960c43c 100644
--- a/spec/requests/api/release/links_spec.rb
+++ b/spec/requests/api/release/links_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Release::Links do
+RSpec.describe API::Release::Links, feature_category: :release_orchestration do
let(:project) { create(:project, :repository, :private) }
let(:maintainer) { create(:user) }
let(:developer) { create(:user) }
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index 754b77af60e..a1aff9a6b1c 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Releases do
+RSpec.describe API::Releases, feature_category: :release_orchestration do
let(:project) { create(:project, :repository, :private) }
let(:maintainer) { create(:user) }
let(:reporter) { create(:user) }
diff --git a/spec/requests/api/remote_mirrors_spec.rb b/spec/requests/api/remote_mirrors_spec.rb
index 338647224e0..3da1760e319 100644
--- a/spec/requests/api/remote_mirrors_spec.rb
+++ b/spec/requests/api/remote_mirrors_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::RemoteMirrors do
+RSpec.describe API::RemoteMirrors, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :remote_mirror) }
let_it_be(:developer) { create(:user) { |u| project.add_developer(u) } }
@@ -90,7 +90,9 @@ RSpec.describe API::RemoteMirrors do
}
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']['url']).to eq(["is blocked: Only allowed schemes are ssh, git, http, https"])
+ expect(json_response['message']['url']).to match_array(
+ ["is blocked: Only allowed schemes are http, https, ssh, git"]
+ )
end
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 3c22f918af5..393ada1da4f 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'mime/types'
-RSpec.describe API::Repositories do
+RSpec.describe API::Repositories, feature_category: :source_code_management do
include RepoHelpers
include WorkhorseHelpers
include ProjectForksHelper
@@ -300,7 +300,7 @@ RSpec.describe API::Repositories do
type, params = workhorse_send_data
expect(type).to eq('git-archive')
- expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\.tar.gz/)
+ expect(params['ArchivePath']).to match(/#{project.path}-[^.]+\.tar.gz/)
expect(response.parsed_body).to be_empty
end
@@ -312,7 +312,7 @@ RSpec.describe API::Repositories do
type, params = workhorse_send_data
expect(type).to eq('git-archive')
- expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\.zip/)
+ expect(params['ArchivePath']).to match(/#{project.path}-[^.]+\.zip/)
end
it 'returns the repository archive archive.tar.bz2' do
@@ -323,7 +323,7 @@ RSpec.describe API::Repositories do
type, params = workhorse_send_data
expect(type).to eq('git-archive')
- expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\.tar.bz2/)
+ expect(params['ArchivePath']).to match(/#{project.path}-[^.]+\.tar.bz2/)
end
context 'when sha does not exist' do
@@ -342,7 +342,7 @@ RSpec.describe API::Repositories do
type, params = workhorse_send_data
expect(type).to eq('git-archive')
- expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\-#{path}\.tar.gz/)
+ expect(params['ArchivePath']).to match(/#{project.path}-[^.]+-#{path}\.tar.gz/)
end
it 'rate limits user when thresholds hit' do
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
index 24efac3128d..6a89e9a56df 100644
--- a/spec/requests/api/resource_access_tokens_spec.rb
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe API::ResourceAccessTokens do
+RSpec.describe API::ResourceAccessTokens, feature_category: :authentication_and_authorization do
let_it_be(:user) { create(:user) }
let_it_be(:user_non_priviledged) { create(:user) }
diff --git a/spec/requests/api/resource_label_events_spec.rb b/spec/requests/api/resource_label_events_spec.rb
index a4a70d89812..1adffea17b7 100644
--- a/spec/requests/api/resource_label_events_spec.rb
+++ b/spec/requests/api/resource_label_events_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ResourceLabelEvents do
+RSpec.describe API::ResourceLabelEvents, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public, namespace: user.namespace) }
let_it_be(:label) { create(:label, project: project) }
diff --git a/spec/requests/api/resource_milestone_events_spec.rb b/spec/requests/api/resource_milestone_events_spec.rb
index 5c81c2180d7..fe991533c85 100644
--- a/spec/requests/api/resource_milestone_events_spec.rb
+++ b/spec/requests/api/resource_milestone_events_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ResourceMilestoneEvents do
+RSpec.describe API::ResourceMilestoneEvents, feature_category: :team_planning do
let!(:user) { create(:user) }
let!(:project) { create(:project, :public, namespace: user.namespace) }
let!(:milestone) { create(:milestone, project: project) }
diff --git a/spec/requests/api/rpm_project_packages_spec.rb b/spec/requests/api/rpm_project_packages_spec.rb
index 68511795c94..515970f86a1 100644
--- a/spec/requests/api/rpm_project_packages_spec.rb
+++ b/spec/requests/api/rpm_project_packages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe API::RpmProjectPackages do
+RSpec.describe API::RpmProjectPackages, feature_category: :package_registry do
include HttpBasicAuthHelpers
include WorkhorseHelpers
@@ -136,7 +136,7 @@ RSpec.describe API::RpmProjectPackages do
end
describe 'GET /api/v4/projects/:id/packages/rpm/:package_file_id/:filename' do
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: group } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: group, property: 'i_package_rpm_user' } }
let(:url) { "/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}" }
subject { get api(url), headers: headers }
@@ -148,7 +148,10 @@ RSpec.describe API::RpmProjectPackages do
end
describe 'POST /api/v4/projects/:project_id/packages/rpm' do
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: group, user: user } }
+ let(:snowplow_gitlab_standard_context) do
+ { project: project, namespace: group, user: user, property: 'i_package_rpm_user' }
+ end
+
let(:url) { "/projects/#{project.id}/packages/rpm" }
let(:file_upload) { fixture_file_upload('spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm') }
@@ -213,6 +216,19 @@ RSpec.describe API::RpmProjectPackages do
expect(response.body).to match(/File is too large/)
end
end
+
+ context 'when filelists.xml file size too large' do
+ before do
+ create(:rpm_repository_file, :filelists, size: 21.megabytes, project: project)
+ end
+
+ it 'returns an error' do
+ upload_file(params: { file: file_upload }, request_headers: headers)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to match(/Repository packages limit exceeded/)
+ end
+ end
end
def upload_file(params: {}, request_headers: headers)
diff --git a/spec/requests/api/rubygem_packages_spec.rb b/spec/requests/api/rubygem_packages_spec.rb
index a7d461781b8..6f048fa57a8 100644
--- a/spec/requests/api/rubygem_packages_spec.rb
+++ b/spec/requests/api/rubygem_packages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::RubygemPackages do
+RSpec.describe API::RubygemPackages, feature_category: :package_registry do
include PackagesManagerApiSpecHelpers
include WorkhorseHelpers
using RSpec::Parameterized::TableSyntax
@@ -15,7 +15,7 @@ RSpec.describe API::RubygemPackages do
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let_it_be(:headers) { {} }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_rubygems_user' } }
let(:tokens) do
{
@@ -164,7 +164,7 @@ RSpec.describe API::RubygemPackages do
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
let(:headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_rubygems_user' } }
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s))
@@ -323,7 +323,7 @@ RSpec.describe API::RubygemPackages do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
let(:user_headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } }
let(:headers) { user_headers.merge(workhorse_headers) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: snowplow_user } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: snowplow_user, property: 'i_package_rubygems_user' } }
let(:snowplow_user) do
case token_type
when :deploy_token
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 60acf6b71dd..430d3b7d187 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Search do
+RSpec.describe API::Search, feature_category: :global_search do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, :wiki_repo, :public, name: 'awesome project', group: group) }
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 3a9b2d02af5..e93ef52ef03 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
+RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, feature_category: :not_owned do
let(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
@@ -25,6 +25,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['secret_detection_token_revocation_url']).to be_nil
expect(json_response['secret_detection_revocation_token_types_url']).to be_nil
expect(json_response['sourcegraph_public_only']).to be_truthy
+ expect(json_response['default_preferred_language']).to be_a String
expect(json_response['default_project_visibility']).to be_a String
expect(json_response['default_snippet_visibility']).to be_a String
expect(json_response['default_group_visibility']).to be_a String
@@ -55,12 +56,15 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['group_runner_token_expiration_interval']).to be_nil
expect(json_response['project_runner_token_expiration_interval']).to be_nil
expect(json_response['max_export_size']).to eq(0)
+ expect(json_response['max_terraform_state_size_bytes']).to eq(0)
expect(json_response['pipeline_limit_per_project_user_sha']).to eq(0)
expect(json_response['delete_inactive_projects']).to be(false)
expect(json_response['inactive_projects_delete_after_months']).to eq(2)
expect(json_response['inactive_projects_min_size_mb']).to eq(0)
expect(json_response['inactive_projects_send_warning_email_after_months']).to eq(1)
expect(json_response['can_create_group']).to eq(true)
+ expect(json_response['jira_connect_application_key']).to eq(nil)
+ expect(json_response['jira_connect_proxy_url']).to eq(nil)
end
end
@@ -146,6 +150,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
mailgun_events_enabled: true,
mailgun_signing_key: 'MAILGUN_SIGNING_KEY',
max_export_size: 6,
+ max_terraform_state_size_bytes: 1_000,
disabled_oauth_sign_in_sources: 'unknown',
import_sources: 'github,bitbucket',
wiki_page_max_content_bytes: 12345,
@@ -158,7 +163,10 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
inactive_projects_delete_after_months: 24,
inactive_projects_min_size_mb: 10,
inactive_projects_send_warning_email_after_months: 12,
- can_create_group: false
+ can_create_group: false,
+ jira_connect_application_key: '123',
+ jira_connect_proxy_url: 'http://example.com',
+ bulk_import_enabled: false
}
expect(response).to have_gitlab_http_status(:ok)
@@ -207,6 +215,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['mailgun_events_enabled']).to be(true)
expect(json_response['mailgun_signing_key']).to eq('MAILGUN_SIGNING_KEY')
expect(json_response['max_export_size']).to eq(6)
+ 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['wiki_page_max_content_bytes']).to eq(12345)
@@ -220,6 +229,9 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['inactive_projects_min_size_mb']).to eq(10)
expect(json_response['inactive_projects_send_warning_email_after_months']).to eq(12)
expect(json_response['can_create_group']).to eq(false)
+ expect(json_response['jira_connect_application_key']).to eq('123')
+ expect(json_response['jira_connect_proxy_url']).to eq('http://example.com')
+ expect(json_response['bulk_import_enabled']).to be(false)
end
end
diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb
index 302d824e650..1085df97cc7 100644
--- a/spec/requests/api/sidekiq_metrics_spec.rb
+++ b/spec/requests/api/sidekiq_metrics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::SidekiqMetrics do
+RSpec.describe API::SidekiqMetrics, feature_category: :not_owned do
let(:admin) { create(:user, :admin) }
describe 'GET sidekiq/*' do
diff --git a/spec/requests/api/snippet_repository_storage_moves_spec.rb b/spec/requests/api/snippet_repository_storage_moves_spec.rb
index 40d01500ac1..6081531aee9 100644
--- a/spec/requests/api/snippet_repository_storage_moves_spec.rb
+++ b/spec/requests/api/snippet_repository_storage_moves_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::SnippetRepositoryStorageMoves do
+RSpec.describe API::SnippetRepositoryStorageMoves, feature_category: :gitaly do
it_behaves_like 'repository_storage_moves API', 'snippets' do
let_it_be(:container) { create(:snippet, :repository).tap { |snippet| snippet.create_repository } }
let_it_be(:storage_move) { create(:snippet_repository_storage_move, :scheduled, container: container) }
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 9408d1cc248..dd0da0cb887 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Snippets, factory_default: :keep do
+RSpec.describe API::Snippets, factory_default: :keep, feature_category: :source_code_management do
include SnippetHelpers
let_it_be(:admin) { create(:user, :admin) }
diff --git a/spec/requests/api/statistics_spec.rb b/spec/requests/api/statistics_spec.rb
index baffb2792e9..85fed48a077 100644
--- a/spec/requests/api/statistics_spec.rb
+++ b/spec/requests/api/statistics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Statistics, 'Statistics' do
+RSpec.describe API::Statistics, 'Statistics', feature_category: :devops_reports do
include ProjectForksHelper
tables_to_analyze = %w[
projects
diff --git a/spec/requests/api/submodules_spec.rb b/spec/requests/api/submodules_spec.rb
index 9840476ca27..7b041ab7c4b 100644
--- a/spec/requests/api/submodules_spec.rb
+++ b/spec/requests/api/submodules_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Submodules do
+RSpec.describe API::Submodules, feature_category: :source_code_management do
let(:user) { create(:user) }
let!(:project) { create(:project, :repository, namespace: user.namespace) }
let(:guest) { create(:user) { |u| project.add_guest(u) } }
diff --git a/spec/requests/api/suggestions_spec.rb b/spec/requests/api/suggestions_spec.rb
index dfc5d169af6..93b2435c601 100644
--- a/spec/requests/api/suggestions_spec.rb
+++ b/spec/requests/api/suggestions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Suggestions do
+RSpec.describe API::Suggestions, feature_category: :code_review do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index 0f1dbea2e73..51edf4b3b3e 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::SystemHooks do
+RSpec.describe API::SystemHooks, feature_category: :integrations do
let_it_be(:non_admin) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be_with_refind(:hook) { create(:system_hook, url: "http://example.com") }
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 3f2ca2a0938..b02c7135b7b 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Tags do
+RSpec.describe API::Tags, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
@@ -479,4 +479,60 @@ RSpec.describe API::Tags do
end
end
end
+
+ describe 'GET /projects/:id/repository/tags/:tag_name/signature' do
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let(:project_id) { project.id }
+ let(:route) { "/projects/#{project_id}/repository/tags/#{tag_name}/signature" }
+
+ context 'when tag does not exist' do
+ let(:tag_name) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 Tag Not Found' }
+ end
+ end
+
+ context 'unsigned tag' do
+ let(:tag_name) { 'v1.1.0' }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 Signature Not Found' }
+ end
+ end
+
+ context 'x509 signed tag' do
+ let(:tag_name) { 'v1.1.1' }
+ let(:tag) { project.repository.find_tag(tag_name) }
+ let(:signature) { tag.signature }
+ let(:x509_certificate) { signature.x509_certificate }
+ let(:x509_issuer) { x509_certificate.x509_issuer }
+
+ it 'returns correct JSON' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq(
+ 'signature_type' => 'X509',
+ 'verification_status' => signature.verification_status.to_s,
+ 'x509_certificate' => {
+ 'id' => x509_certificate.id,
+ 'subject' => x509_certificate.subject,
+ 'subject_key_identifier' => x509_certificate.subject_key_identifier,
+ 'email' => x509_certificate.email,
+ 'serial_number' => x509_certificate.serial_number,
+ 'certificate_status' => x509_certificate.certificate_status,
+ 'x509_issuer' => {
+ 'id' => x509_issuer.id,
+ 'subject' => x509_issuer.subject,
+ 'subject_key_identifier' => x509_issuer.subject_key_identifier,
+ 'crl_url' => x509_issuer.crl_url
+ }
+ }
+ )
+ end
+ end
+ end
end
diff --git a/spec/requests/api/task_completion_status_spec.rb b/spec/requests/api/task_completion_status_spec.rb
index 97ce858ba12..c46d6954da3 100644
--- a/spec/requests/api/task_completion_status_spec.rb
+++ b/spec/requests/api/task_completion_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'task completion status response' do
+RSpec.describe 'task completion status response', features: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) do
create(:project, :public, creator_id: user.id, namespace: user.namespace)
@@ -10,44 +10,44 @@ RSpec.describe 'task completion status response' do
shared_examples 'taskable completion status provider' do |path|
samples = [
- {
- description: '',
- expected_count: 0,
- expected_completed_count: 0
- },
- {
- description: 'Lorem ipsum',
- expected_count: 0,
- expected_completed_count: 0
- },
- {
- description: %{- [ ] task 1
+ {
+ description: '',
+ expected_count: 0,
+ expected_completed_count: 0
+ },
+ {
+ description: 'Lorem ipsum',
+ expected_count: 0,
+ expected_completed_count: 0
+ },
+ {
+ description: %{- [ ] task 1
- [x] task 2 },
- expected_count: 2,
- expected_completed_count: 1
- },
- {
- description: %{- [ ] task 1
+ expected_count: 2,
+ expected_completed_count: 1
+ },
+ {
+ description: %{- [ ] task 1
- [ ] task 2 },
- expected_count: 2,
- expected_completed_count: 0
- },
- {
- description: %{- [x] task 1
+ expected_count: 2,
+ expected_completed_count: 0
+ },
+ {
+ description: %{- [x] task 1
- [x] task 2 },
- expected_count: 2,
- expected_completed_count: 2
- },
- {
- description: %{- [ ] task 1},
- expected_count: 1,
- expected_completed_count: 0
- },
- {
- description: %{- [x] task 1},
- expected_count: 1,
- expected_completed_count: 1
- }
+ expected_count: 2,
+ expected_completed_count: 2
+ },
+ {
+ description: %{- [ ] task 1},
+ expected_count: 1,
+ expected_completed_count: 0
+ },
+ {
+ description: %{- [x] task 1},
+ expected_count: 1,
+ expected_completed_count: 1
+ }
]
samples.each do |sample_data|
context "with a description of #{sample_data[:description].inspect}" do
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index adb37c62dc3..8782c3cba4b 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Templates do
+RSpec.describe API::Templates, feature_category: :source_code_management do
context 'the Template Entity' do
before do
get api('/templates/gitignores/Ruby')
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index ae61017f5bb..2bd7cb027aa 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Terraform::Modules::V1::Packages do
+RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package_registry do
include PackagesManagerApiSpecHelpers
include WorkhorseHelpers
using RSpec::Parameterized::TableSyntax
@@ -418,7 +418,8 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
{
project: project,
user: user_role == :anonymous ? nil : user,
- namespace: project.namespace
+ namespace: project.namespace,
+ property: 'i_package_terraform_module_user'
}
end
@@ -583,7 +584,10 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
with_them do
let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } }
let(:headers) { user_headers.merge(workhorse_headers) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: snowplow_user } }
+ let(:snowplow_gitlab_standard_context) do
+ { project: project, namespace: project.namespace, user: snowplow_user, property: 'i_package_terraform_module_user' }
+ end
+
let(:snowplow_user) do
case token_type
when :deploy_token
diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb
index 38b08b4e214..fd34345d814 100644
--- a/spec/requests/api/terraform/state_spec.rb
+++ b/spec/requests/api/terraform/state_spec.rb
@@ -2,20 +2,22 @@
require 'spec_helper'
-RSpec.describe API::Terraform::State, :snowplow do
+RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructure_as_code do
include HttpBasicAuthHelpers
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user, developer_projects: [project]) }
let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) }
- let!(:state) { create(:terraform_state, :with_version, project: project) }
-
let(:current_user) { maintainer }
let(:auth_header) { user_basic_auth_header(current_user) }
let(:project_id) { project.id }
- let(:state_name) { state.name }
+
+ let(:state_name) { "some-state" }
let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name}" }
+ let!(:state) do
+ create(:terraform_state, :with_version, project: project, name: URI.decode_www_form_component(state_name))
+ end
before do
stub_terraform_state_object_storage
@@ -91,15 +93,35 @@ RSpec.describe API::Terraform::State, :snowplow do
end
end
- context 'personal acceess token authentication' do
+ shared_examples 'can access terraform state' do
+ it 'returns terraform state of a project of given state name' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(state.reload.latest_file.read)
+ end
+ end
+
+ context 'personal access token authentication' do
context 'with maintainer permissions' do
let(:current_user) { maintainer }
- it 'returns terraform state belonging to a project of given state name' do
- request
+ where(given_state_name: %w[test-state test.state test%2Ffoo])
+ with_them do
+ it_behaves_like 'can access terraform state' do
+ let(:state_name) { given_state_name }
+ end
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to eq(state.reload.latest_file.read)
+ context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do
+ let(:state_name) { 'state-name-with-dot' }
+ let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name}.tfstate" }
+
+ before do
+ stub_feature_flags(allow_dots_on_tf_state_names: false)
+ end
+
+ it_behaves_like 'can access terraform state'
end
context 'for a project that does not exist' do
@@ -112,18 +134,23 @@ RSpec.describe API::Terraform::State, :snowplow do
end
end
+ context 'with invalid state name' do
+ let(:state_name) { 'foo/bar' }
+
+ it 'returns a 404 error' do
+ request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
it_behaves_like 'cannot access a state that is scheduled for deletion'
end
context 'with developer permissions' do
let(:current_user) { developer }
- it 'returns terraform state belonging to a project of given state name' do
- request
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to eq(state.reload.latest_file.read)
- end
+ it_behaves_like 'can access terraform state'
end
end
@@ -133,12 +160,7 @@ RSpec.describe API::Terraform::State, :snowplow do
context 'with maintainer permissions' do
let(:job) { create(:ci_build, status: :running, project: project, user: maintainer) }
- it 'returns terraform state belonging to a project of given state name' do
- request
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to eq(state.reload.latest_file.read)
- end
+ it_behaves_like 'can access terraform state'
it 'returns unauthorized if the the job is not running' do
job.update!(status: :failed)
@@ -161,12 +183,7 @@ RSpec.describe API::Terraform::State, :snowplow do
context 'with developer permissions' do
let(:job) { create(:ci_build, status: :running, project: project, user: developer) }
- it 'returns terraform state belonging to a project of given state name' do
- request
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to eq(state.reload.latest_file.read)
- end
+ it_behaves_like 'can access terraform state'
end
end
end
@@ -182,11 +199,26 @@ RSpec.describe API::Terraform::State, :snowplow do
context 'with maintainer permissions' do
let(:current_user) { maintainer }
- it 'updates the state' do
- expect { request }.to change { Terraform::State.count }.by(0)
+ where(given_state_name: %w[test-state test.state test%2Ffoo])
+ with_them do
+ let(:state_name) { given_state_name }
- expect(response).to have_gitlab_http_status(:ok)
- expect(Gitlab::Json.parse(response.body)).to be_empty
+ it 'updates the state' do
+ expect { request }.to change { Terraform::State.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Gitlab::Json.parse(response.body)).to be_empty
+ end
+ end
+
+ context 'with invalid state name' do
+ let(:state_name) { 'foo/bar' }
+
+ it 'returns a 404 error' do
+ request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
context 'when serial already exists' do
@@ -224,16 +256,39 @@ RSpec.describe API::Terraform::State, :snowplow do
end
context 'when there is no terraform state of a given name' do
- let(:state_name) { 'example2' }
+ let(:non_existing_state_name) { 'non-existing-state' }
+ let(:non_existing_state_path) { "/projects/#{project_id}/terraform/state/#{non_existing_state_name}" }
+
+ subject(:request) { post api(non_existing_state_path), headers: auth_header, as: :json, params: params }
context 'with maintainer permissions' do
let(:current_user) { maintainer }
- it 'creates a new state' do
- expect { request }.to change { Terraform::State.count }.by(1)
+ where(given_state_name: %w[test-state test.state test%2Ffoo])
+ with_them do
+ let(:state_name) { given_state_name }
- expect(response).to have_gitlab_http_status(:ok)
- expect(Gitlab::Json.parse(response.body)).to be_empty
+ it 'creates a new state' do
+ expect { request }.to change { Terraform::State.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Gitlab::Json.parse(response.body)).to be_empty
+ end
+ end
+
+ context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do
+ let(:non_existing_state_name) { 'state-name-with-dot.tfstate' }
+
+ before do
+ stub_feature_flags(allow_dots_on_tf_state_names: false)
+ end
+
+ it 'strips characters after the dot' do
+ expect { request }.to change { Terraform::State.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Terraform::State.last.name).to eq('state-name-with-dot')
+ end
end
end
@@ -269,6 +324,48 @@ RSpec.describe API::Terraform::State, :snowplow do
expect(state.reload_latest_version.build).to eq(job)
end
end
+
+ describe 'response depending on the max allowed state size' do
+ let(:current_user) { maintainer }
+
+ before do
+ stub_application_setting(max_terraform_state_size_bytes: max_allowed_state_size)
+
+ request
+ end
+
+ context 'when the max allowed state size is unlimited (set as 0)' do
+ let(:max_allowed_state_size) { 0 }
+
+ it 'returns a success response' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the max allowed state size is greater than the request state size' do
+ let(:max_allowed_state_size) { params.to_json.size + 1 }
+
+ it 'returns a success response' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the max allowed state size is equal to the request state size' do
+ let(:max_allowed_state_size) { params.to_json.size }
+
+ it 'returns a success response' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the max allowed state size is less than the request state size' do
+ let(:max_allowed_state_size) { params.to_json.size - 1 }
+
+ it "returns a 'payload too large' response" do
+ expect(response).to have_gitlab_http_status(:payload_too_large)
+ end
+ end
+ end
end
describe 'DELETE /projects/:id/terraform/state/:name' do
@@ -276,11 +373,8 @@ RSpec.describe API::Terraform::State, :snowplow do
it_behaves_like 'endpoint with unique user tracking'
- context 'with maintainer permissions' do
- let(:current_user) { maintainer }
- let(:deletion_service) { instance_double(Terraform::States::TriggerDestroyService) }
-
- it 'schedules the state for deletion and returns empty body' do
+ shared_examples 'schedules the state for deletion' do
+ it 'returns empty body' do
expect(Terraform::States::TriggerDestroyService).to receive(:new).and_return(deletion_service)
expect(deletion_service).to receive(:execute).once
@@ -289,6 +383,40 @@ RSpec.describe API::Terraform::State, :snowplow do
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)).to be_empty
end
+ end
+
+ context 'with maintainer permissions' do
+ let(:current_user) { maintainer }
+ let(:deletion_service) { instance_double(Terraform::States::TriggerDestroyService) }
+
+ where(given_state_name: %w[test-state test.state test%2Ffoo])
+ with_them do
+ let(:state_name) { given_state_name }
+
+ it_behaves_like 'schedules the state for deletion'
+ end
+
+ context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do
+ let(:state_name) { 'state-name-with-dot' }
+ let(:state_name_with_dot) { "#{state_name}.tfstate" }
+ let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name_with_dot}" }
+
+ before do
+ stub_feature_flags(allow_dots_on_tf_state_names: false)
+ end
+
+ it_behaves_like 'schedules the state for deletion'
+ end
+
+ context 'with invalid state name' do
+ let(:state_name) { 'foo/bar' }
+
+ it 'returns a 404 error' do
+ request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
it_behaves_like 'cannot access a state that is scheduled for deletion'
end
@@ -304,7 +432,7 @@ RSpec.describe API::Terraform::State, :snowplow do
end
end
- describe 'PUT /projects/:id/terraform/state/:name/lock' do
+ describe 'POST /projects/:id/terraform/state/:name/lock' do
let(:params) do
{
ID: '123-456',
@@ -322,10 +450,14 @@ RSpec.describe API::Terraform::State, :snowplow do
it_behaves_like 'endpoint with unique user tracking'
it_behaves_like 'cannot access a state that is scheduled for deletion'
- it 'locks the terraform state' do
- request
+ context 'with invalid state name' do
+ let(:state_name) { 'foo/bar' }
- expect(response).to have_gitlab_http_status(:ok)
+ it 'returns a 404 error' do
+ request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
context 'state is already locked' do
@@ -349,6 +481,47 @@ RSpec.describe API::Terraform::State, :snowplow do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
+
+ where(given_state_name: %w[test-state test%2Ffoo])
+ with_them do
+ let(:state_name) { given_state_name }
+
+ it 'locks the terraform state' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with a dot in the state name' do
+ let(:state_name) { 'test.state' }
+
+ context 'with allow_dots_on_tf_state_names ff enabled' do
+ before do
+ stub_feature_flags(allow_dots_on_tf_state_names: true)
+ end
+
+ let(:state_name) { 'test.state' }
+
+ it 'locks the terraform state' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with allow_dots_on_tf_state_names ff disabled' do
+ before do
+ stub_feature_flags(allow_dots_on_tf_state_names: false)
+ end
+
+ it 'returns 404' do
+ request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
end
describe 'DELETE /projects/:id/terraform/state/:name/lock' do
@@ -365,8 +538,9 @@ RSpec.describe API::Terraform::State, :snowplow do
end
before do
- state.lock_xid = '123-456'
+ state.lock_xid = '123.456'
state.save!
+ stub_feature_flags(allow_dots_on_tf_state_names: true)
end
subject(:request) { delete api("#{state_path}/lock"), headers: auth_header, params: params }
@@ -379,28 +553,61 @@ RSpec.describe API::Terraform::State, :snowplow do
let(:lock_id) { 'irrelevant to this test, just needs to be present' }
end
- context 'with the correct lock id' do
- let(:lock_id) { '123-456' }
+ where(given_state_name: %w[test-state test.state test%2Ffoo])
+ with_them do
+ let(:state_name) { given_state_name }
- it 'removes the terraform state lock' do
- request
+ context 'with the correct lock id' do
+ let(:lock_id) { '123.456' }
- expect(response).to have_gitlab_http_status(:ok)
+ it 'removes the terraform state lock' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with allow_dots_on_tf_state_names ff disabled' do
+ before do
+ stub_feature_flags(allow_dots_on_tf_state_names: false)
+ end
+
+ context 'with dots in the state name' do
+ let(:lock_id) { '123.456' }
+ let(:state_name) { 'test.state' }
+
+ it 'returns 404' do
+ request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'with no lock id (force-unlock)' do
+ let(:params) { {} }
+
+ it 'removes the terraform state lock' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
end
- context 'with no lock id (force-unlock)' do
- let(:params) { {} }
+ context 'with invalid state name' do
+ let(:lock_id) { '123.456' }
+ let(:state_name) { 'foo/bar' }
- it 'removes the terraform state lock' do
+ it 'returns a 404 error' do
request
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with an incorrect lock id' do
- let(:lock_id) { '456-789' }
+ let(:lock_id) { '456.789' }
it 'returns an error' do
request
@@ -420,7 +627,7 @@ RSpec.describe API::Terraform::State, :snowplow do
end
context 'user does not have permission to unlock the state' do
- let(:lock_id) { '123-456' }
+ let(:lock_id) { '123.456' }
let(:current_user) { developer }
it 'returns an error' do
diff --git a/spec/requests/api/terraform/state_version_spec.rb b/spec/requests/api/terraform/state_version_spec.rb
index ade0aacf805..28abbb5749d 100644
--- a/spec/requests/api/terraform/state_version_spec.rb
+++ b/spec/requests/api/terraform/state_version_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Terraform::StateVersion do
+RSpec.describe API::Terraform::StateVersion, feature_category: :infrastructure_as_code do
include HttpBasicAuthHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 7a626ee4d29..5a342f79926 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Todos do
+RSpec.describe API::Todos, feature_category: :source_code_management do
include DesignManagementTestHelpers
let_it_be(:group) { create(:group) }
@@ -15,6 +15,7 @@ RSpec.describe API::Todos do
let_it_be(:work_item) { create(:work_item, :task, project: project_1) }
let_it_be(:merge_request) { create(:merge_request, source_project: project_1) }
let_it_be(:alert) { create(:alert_management_alert, project: project_1) }
+ let_it_be(:group_request_todo) { create(:todo, author: author_1, user: john_doe, target: group, action: Todo::MEMBER_ACCESS_REQUESTED) }
let_it_be(:alert_todo) { create(:todo, project: project_1, author: john_doe, user: john_doe, target: alert) }
let_it_be(:merge_request_todo) { create(:todo, project: project_1, author: author_2, user: john_doe, target: merge_request) }
let_it_be(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe, target: issue) }
@@ -71,7 +72,7 @@ RSpec.describe API::Todos do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(6)
+ expect(json_response.length).to eq(7)
expect(json_response[0]).to include(
'id' => pending_5.id,
@@ -127,6 +128,17 @@ RSpec.describe API::Todos do
'title' => alert.title
)
)
+
+ expect(json_response[6]).to include(
+ 'target_type' => 'Namespace',
+ 'action_name' => 'member_access_requested',
+ 'target' => hash_including(
+ 'id' => group.id,
+ 'name' => group.name,
+ 'full_path' => group.full_path
+ ),
+ 'target_url' => Gitlab::Routing.url_helpers.group_group_members_url(group, tab: 'access_requests')
+ )
end
context "when current user does not have access to one of the TODO's target" do
@@ -137,7 +149,7 @@ RSpec.describe API::Todos do
get api('/todos', john_doe)
- expect(json_response.count).to eq(6)
+ expect(json_response.count).to eq(7)
expect(json_response.map { |t| t['id'] }).not_to include(no_access_todo.id, pending_4.id)
end
end
@@ -231,7 +243,7 @@ RSpec.describe API::Todos do
create(:on_commit_todo, project: new_todo.project, author: author_1, user: john_doe, target: merge_request_3)
create(:todo, project: new_todo.project, author: author_2, user: john_doe, target: merge_request_3)
- expect { get api('/todos', john_doe) }.not_to exceed_query_limit(control1).with_threshold(5)
+ expect { get api('/todos', john_doe) }.not_to exceed_query_limit(control1).with_threshold(6)
control2 = ActiveRecord::QueryRecorder.new { get api('/todos', john_doe) }
create_issue_todo_for(john_doe)
diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb
index 1ad6f876fab..14719292557 100644
--- a/spec/requests/api/topics_spec.rb
+++ b/spec/requests/api/topics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Topics do
+RSpec.describe API::Topics, feature_category: :projects do
include WorkhorseHelpers
let_it_be(:file) { fixture_file_upload('spec/fixtures/dk.png') }
diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb
index 4d382f91023..5daf7cd7b75 100644
--- a/spec/requests/api/unleash_spec.rb
+++ b/spec/requests/api/unleash_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Unleash do
+RSpec.describe API::Unleash, feature_category: :feature_flags do
include FeatureFlagHelpers
let_it_be(:project, refind: true) { create(:project) }
@@ -88,85 +88,6 @@ RSpec.describe API::Unleash do
end
end
- shared_examples_for 'support multiple environments' do
- let!(:client) { create(:operations_feature_flags_client, project: project) }
- let!(:base_headers) { { "UNLEASH-INSTANCEID" => client.token } }
- let!(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "test" }) }
-
- let!(:feature_flag_1) do
- create(:operations_feature_flag, name: "feature_flag_1", project: project, active: true)
- end
-
- let!(:feature_flag_2) do
- create(:operations_feature_flag, name: "feature_flag_2", project: project, active: false)
- end
-
- before do
- create_scope(feature_flag_1, 'production', false)
- create_scope(feature_flag_2, 'review/*', true)
- end
-
- it 'does not have N+1 problem' do
- control_count = ActiveRecord::QueryRecorder.new { get api(features_url), headers: headers }.count
-
- create(:operations_feature_flag, name: "feature_flag_3", project: project, active: true)
-
- expect { get api(features_url), headers: headers }.not_to exceed_query_limit(control_count)
- end
-
- context 'when app name is staging' do
- let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "staging" }) }
-
- it 'returns correct active values' do
- subject
-
- feature_flag_1 = json_response['features'].find { |f| f['name'] == 'feature_flag_1' }
- feature_flag_2 = json_response['features'].find { |f| f['name'] == 'feature_flag_2' }
-
- expect(feature_flag_1['enabled']).to eq(true)
- expect(feature_flag_2['enabled']).to eq(false)
- end
- end
-
- context 'when app name is production' do
- let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "production" }) }
-
- it 'returns correct active values' do
- subject
-
- feature_flag_1 = json_response['features'].find { |f| f['name'] == 'feature_flag_1' }
- feature_flag_2 = json_response['features'].find { |f| f['name'] == 'feature_flag_2' }
-
- expect(feature_flag_1['enabled']).to eq(false)
- expect(feature_flag_2['enabled']).to eq(false)
- end
- end
-
- context 'when app name is review/patch-1' do
- let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "review/patch-1" }) }
-
- it 'returns correct active values' do
- subject
-
- feature_flag_1 = json_response['features'].find { |f| f['name'] == 'feature_flag_1' }
- feature_flag_2 = json_response['features'].find { |f| f['name'] == 'feature_flag_2' }
-
- expect(feature_flag_1['enabled']).to eq(true)
- expect(feature_flag_2['enabled']).to eq(false)
- end
- end
-
- context 'when app name is empty' do
- let(:headers) { base_headers }
-
- it 'returns empty list' do
- subject
-
- expect(json_response['features'].count).to eq(0)
- end
- end
- end
-
%w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint|
describe "GET #{features_endpoint}", :use_clean_rails_redis_caching do
let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) }
diff --git a/spec/requests/api/usage_data_non_sql_metrics_spec.rb b/spec/requests/api/usage_data_non_sql_metrics_spec.rb
index 0b73d0f96a4..0a6f248af2c 100644
--- a/spec/requests/api/usage_data_non_sql_metrics_spec.rb
+++ b/spec/requests/api/usage_data_non_sql_metrics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::UsageDataNonSqlMetrics do
+RSpec.describe API::UsageDataNonSqlMetrics, feature_category: :service_ping do
include UsageDataHelpers
let_it_be(:admin) { create(:user, admin: true) }
diff --git a/spec/requests/api/usage_data_queries_spec.rb b/spec/requests/api/usage_data_queries_spec.rb
index 6ce03954246..e556064025c 100644
--- a/spec/requests/api/usage_data_queries_spec.rb
+++ b/spec/requests/api/usage_data_queries_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'rake_helper'
-RSpec.describe API::UsageDataQueries do
+RSpec.describe API::UsageDataQueries, feature_category: :service_ping do
include UsageDataHelpers
let_it_be(:admin) { create(:user, admin: true) }
@@ -80,7 +80,7 @@ RSpec.describe API::UsageDataQueries do
end
it 'matches the generated query' do
- Timecop.freeze(2021, 1, 1) do
+ travel_to(Time.utc(2021, 1, 1)) do
get api(endpoint, admin)
end
diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb
index d532fb6c168..935ddbf4764 100644
--- a/spec/requests/api/usage_data_spec.rb
+++ b/spec/requests/api/usage_data_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::UsageData do
+RSpec.describe API::UsageData, feature_category: :service_ping do
let_it_be(:user) { create(:user) }
describe 'POST /usage_data/increment_counter' do
diff --git a/spec/requests/api/user_counts_spec.rb b/spec/requests/api/user_counts_spec.rb
index 369ae49de08..27e5311e2eb 100644
--- a/spec/requests/api/user_counts_spec.rb
+++ b/spec/requests/api/user_counts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::UserCounts do
+RSpec.describe API::UserCounts, feature_category: :service_ping do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
diff --git a/spec/requests/api/users_preferences_spec.rb b/spec/requests/api/users_preferences_spec.rb
index 97e37263ee6..53f366371e5 100644
--- a/spec/requests/api/users_preferences_spec.rb
+++ b/spec/requests/api/users_preferences_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Users do
+RSpec.describe API::Users, feature_category: :users do
let_it_be(:user) { create(:user) }
describe 'PUT /user/preferences/' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 6688a998a1a..bfb71d95f5e 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Users do
+RSpec.describe API::Users, feature_category: :users do
include WorkhorseHelpers
let_it_be(:admin) { create(:admin) }
@@ -1988,11 +1988,19 @@ RSpec.describe API::Users do
expect(json_response['error']).to eq('title is missing')
end
- it "creates ssh key" do
- key_attrs = attributes_for :key
+ it "creates ssh key", :aggregate_failures do
+ key_attrs = attributes_for(:key, usage_type: :signing)
+
expect do
post api("/users/#{user.id}/keys", admin), params: key_attrs
end.to change { user.keys.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+
+ key = user.keys.last
+ expect(key.title).to eq(key_attrs[:title])
+ expect(key.key).to eq(key_attrs[:key])
+ expect(key.usage_type).to eq(key_attrs[:usage_type].to_s)
end
it 'creates SSH key with `expires_at` attribute' do
@@ -2848,12 +2856,19 @@ RSpec.describe API::Users do
end
describe "POST /user/keys" do
- it "creates ssh key" do
- key_attrs = attributes_for :key
+ it "creates ssh key", :aggregate_failures do
+ key_attrs = attributes_for(:key, usage_type: :signing)
+
expect do
post api("/user/keys", user), params: key_attrs
end.to change { user.keys.count }.by(1)
+
expect(response).to have_gitlab_http_status(:created)
+
+ key = user.keys.last
+ expect(key.title).to eq(key_attrs[:title])
+ expect(key.key).to eq(key_attrs[:key])
+ expect(key.usage_type).to eq(key_attrs[:usage_type].to_s)
end
it 'creates SSH key with `expires_at` attribute' do
diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb
index 5bfea15f0ca..0b8fac5c55c 100644
--- a/spec/requests/api/v3/github_spec.rb
+++ b/spec/requests/api/v3/github_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::V3::Github do
+RSpec.describe API::V3::Github, feature_category: :integrations do
let_it_be(:user) { create(:user) }
let_it_be(:unauthorized_user) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index f4096eef8d0..00e38a5bb7e 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -12,7 +12,7 @@ require 'spec_helper'
# - maintainer
# because they are 3 edge cases of using wiki pages.
-RSpec.describe API::Wikis do
+RSpec.describe API::Wikis, feature_category: :wiki do
include WorkhorseHelpers
include AfterNextHelpers
diff --git a/spec/requests/concerns/planning_hierarchy_spec.rb b/spec/requests/concerns/planning_hierarchy_spec.rb
index ece9270b3a1..89232916936 100644
--- a/spec/requests/concerns/planning_hierarchy_spec.rb
+++ b/spec/requests/concerns/planning_hierarchy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PlanningHierarchy, type: :request do
+RSpec.describe PlanningHierarchy, type: :request, feature_category: :projects 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/requests/content_security_policy_spec.rb b/spec/requests/content_security_policy_spec.rb
index 06fc5b0e190..3f0665f1ce5 100644
--- a/spec/requests/content_security_policy_spec.rb
+++ b/spec/requests/content_security_policy_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
# The AnonymousController doesn't support setting the CSP
# This is why an arbitrary test request was chosen instead
# of testing in application_controller_spec.
-RSpec.describe 'Content Security Policy' do
+RSpec.describe 'Content Security Policy', feature_category: :application_instrumentation do
let(:snowplow_host) { 'snowplow.example.com' }
shared_examples 'snowplow is not in the CSP' do
diff --git a/spec/requests/dashboard/projects_controller_spec.rb b/spec/requests/dashboard/projects_controller_spec.rb
index 4cd3b6c4f9e..752799196c9 100644
--- a/spec/requests/dashboard/projects_controller_spec.rb
+++ b/spec/requests/dashboard/projects_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Dashboard::ProjectsController do
+RSpec.describe Dashboard::ProjectsController, feature_category: :projects do
context 'token authentication' do
it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: false do
let(:url) { dashboard_projects_url(:atom) }
diff --git a/spec/requests/dashboard_controller_spec.rb b/spec/requests/dashboard_controller_spec.rb
index 62655d720c5..9edacb27c93 100644
--- a/spec/requests/dashboard_controller_spec.rb
+++ b/spec/requests/dashboard_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DashboardController do
+RSpec.describe DashboardController, feature_category: :authentication_and_authorization do
context 'token authentication' do
it_behaves_like 'authenticates sessionless user for the request spec', 'issues atom', public_resource: false do
let(:url) { issues_dashboard_url(:atom, assignee_username: user.username) }
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 20d298edfe5..66337b94c75 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Git HTTP requests' do
+RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
include ProjectForksHelper
include TermsHelper
include GitHttpHelpers
diff --git a/spec/requests/groups/autocomplete_sources_spec.rb b/spec/requests/groups/autocomplete_sources_spec.rb
index d053e0fe773..e44fb9f6c37 100644
--- a/spec/requests/groups/autocomplete_sources_spec.rb
+++ b/spec/requests/groups/autocomplete_sources_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'groups autocomplete' do
+RSpec.describe 'groups autocomplete', feature_category: :subgroups do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:group) { create(:group, :private) }
diff --git a/spec/requests/groups/clusters/integrations_controller_spec.rb b/spec/requests/groups/clusters/integrations_controller_spec.rb
index 29e37e2e48c..0b9148e917b 100644
--- a/spec/requests/groups/clusters/integrations_controller_spec.rb
+++ b/spec/requests/groups/clusters/integrations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Clusters::IntegrationsController do
+RSpec.describe Groups::Clusters::IntegrationsController, features: :integrations do
include AccessMatchersForController
shared_examples 'a secure endpoint' do
diff --git a/spec/requests/groups/crm/contacts_controller_spec.rb b/spec/requests/groups/crm/contacts_controller_spec.rb
index 70086ddbbba..4916ce60108 100644
--- a/spec/requests/groups/crm/contacts_controller_spec.rb
+++ b/spec/requests/groups/crm/contacts_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Crm::ContactsController do
+RSpec.describe Groups::Crm::ContactsController, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
shared_examples 'response with 404 status' do
diff --git a/spec/requests/groups/crm/organizations_controller_spec.rb b/spec/requests/groups/crm/organizations_controller_spec.rb
index e841dd80b67..3e7e9a8e878 100644
--- a/spec/requests/groups/crm/organizations_controller_spec.rb
+++ b/spec/requests/groups/crm/organizations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Crm::OrganizationsController do
+RSpec.describe Groups::Crm::OrganizationsController, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
shared_examples 'response with 404 status' do
diff --git a/spec/requests/groups/deploy_tokens_controller_spec.rb b/spec/requests/groups/deploy_tokens_controller_spec.rb
index b3dce9b9cf1..05fd8d9691c 100644
--- a/spec/requests/groups/deploy_tokens_controller_spec.rb
+++ b/spec/requests/groups/deploy_tokens_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::DeployTokensController do
+RSpec.describe Groups::DeployTokensController, feature_category: :continuous_delivery do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:deploy_token) { create(:deploy_token, :group, groups: [group]) }
diff --git a/spec/requests/groups/email_campaigns_controller_spec.rb b/spec/requests/groups/email_campaigns_controller_spec.rb
index 4d630ef6710..7db5c084793 100644
--- a/spec/requests/groups/email_campaigns_controller_spec.rb
+++ b/spec/requests/groups/email_campaigns_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::EmailCampaignsController do
+RSpec.describe Groups::EmailCampaignsController, feature_category: :navigation do
using RSpec::Parameterized::TableSyntax
describe 'GET #index', :snowplow do
diff --git a/spec/requests/groups/harbor/artifacts_controller_spec.rb b/spec/requests/groups/harbor/artifacts_controller_spec.rb
index ea9529119a6..7ec792d081d 100644
--- a/spec/requests/groups/harbor/artifacts_controller_spec.rb
+++ b/spec/requests/groups/harbor/artifacts_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Harbor::ArtifactsController do
+RSpec.describe Groups::Harbor::ArtifactsController, feature_category: :build_artifacts do
it_behaves_like 'a harbor artifacts controller', anonymous_status_code: '404' do
let_it_be(:container) { create(:group) }
let_it_be(:harbor_integration) { create(:harbor_integration, group: container, project: nil) }
diff --git a/spec/requests/groups/harbor/repositories_controller_spec.rb b/spec/requests/groups/harbor/repositories_controller_spec.rb
index b4022561f54..f397e9192ea 100644
--- a/spec/requests/groups/harbor/repositories_controller_spec.rb
+++ b/spec/requests/groups/harbor/repositories_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Harbor::RepositoriesController do
+RSpec.describe Groups::Harbor::RepositoriesController, feature_category: :source_code_management do
it_behaves_like 'a harbor repositories controller', anonymous_status_code: '404' do
let_it_be(:container, reload: true) { create(:group) }
let_it_be(:harbor_integration) { create(:harbor_integration, group: container, project: nil) }
diff --git a/spec/requests/groups/harbor/tags_controller_spec.rb b/spec/requests/groups/harbor/tags_controller_spec.rb
index 257d4366837..15da1cb1c4b 100644
--- a/spec/requests/groups/harbor/tags_controller_spec.rb
+++ b/spec/requests/groups/harbor/tags_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Harbor::TagsController do
+RSpec.describe Groups::Harbor::TagsController, feature_category: :source_code_management do
it_behaves_like 'a harbor tags controller', anonymous_status_code: '404' do
let_it_be(:container) { create(:group) }
let_it_be(:harbor_integration) { create(:harbor_integration, group: container, project: nil) }
diff --git a/spec/requests/groups/milestones_controller_spec.rb b/spec/requests/groups/milestones_controller_spec.rb
index e6418c7694d..54a25333c02 100644
--- a/spec/requests/groups/milestones_controller_spec.rb
+++ b/spec/requests/groups/milestones_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::MilestonesController do
+RSpec.describe Groups::MilestonesController, feature_category: :team_planning do
context 'N+1 DB queries' do
let_it_be(:user) { create(:user) }
let_it_be(:public_group) { create(:group, :public) }
diff --git a/spec/requests/groups/observability_controller_spec.rb b/spec/requests/groups/observability_controller_spec.rb
index a08231fe939..46690d60539 100644
--- a/spec/requests/groups/observability_controller_spec.rb
+++ b/spec/requests/groups/observability_controller_spec.rb
@@ -2,14 +2,13 @@
require 'spec_helper'
-RSpec.describe Groups::ObservabilityController do
- include ContentSecurityPolicyHelpers
-
+RSpec.describe Groups::ObservabilityController, feature_category: :tracing do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let(:observability_url) { Gitlab::Observability.observability_url }
- let(:expected_observability_path) { "/" }
+ let(:path) { nil }
+ let(:expected_observability_path) { nil }
shared_examples 'observability route request' do
subject do
@@ -17,6 +16,10 @@ RSpec.describe Groups::ObservabilityController do
response
end
+ it_behaves_like 'observability csp policy' do
+ let(:tested_path) { path }
+ end
+
context 'when user is not authenticated' do
it 'returns 404' do
expect(subject).to have_gitlab_http_status(:not_found)
@@ -70,101 +73,22 @@ RSpec.describe Groups::ObservabilityController do
describe 'GET #dashboards' do
let(:path) { group_observability_dashboards_path(group) }
- let(:expected_observability_path) { "#{observability_url}/#{group.id}/" }
+ let(:expected_observability_path) { "#{observability_url}/-/#{group.id}/" }
it_behaves_like 'observability route request'
end
describe 'GET #manage' do
let(:path) { group_observability_manage_path(group) }
- let(:expected_observability_path) { "#{observability_url}/#{group.id}/dashboards" }
+ let(:expected_observability_path) { "#{observability_url}/-/#{group.id}/dashboards" }
it_behaves_like 'observability route request'
end
describe 'GET #explore' do
let(:path) { group_observability_explore_path(group) }
- let(:expected_observability_path) { "#{observability_url}/#{group.id}/explore" }
+ let(:expected_observability_path) { "#{observability_url}/-/#{group.id}/explore" }
it_behaves_like 'observability route request'
end
-
- describe 'CSP' do
- before do
- setup_csp_for_controller(described_class, csp)
- end
-
- subject do
- get group_observability_dashboards_path(group)
- response.headers['Content-Security-Policy']
- end
-
- context 'when there is no CSP config' do
- let(:csp) { ActionDispatch::ContentSecurityPolicy.new }
-
- it 'does not add any csp header' do
- expect(subject).to be_blank
- end
- end
-
- context 'when frame-src exists in the CSP config' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.frame_src 'https://something.test'
- end
- end
-
- it 'appends the proper url to frame-src CSP directives' do
- expect(subject).to include(
- "frame-src https://something.test #{observability_url} 'self'")
- end
- end
-
- context 'when self is already present in the policy' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.frame_src "'self'"
- end
- end
-
- it 'does not append self again' do
- expect(subject).to include(
- "frame-src 'self' #{observability_url};")
- end
- end
-
- context 'when default-src exists in the CSP config' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.default_src 'https://something.test'
- end
- end
-
- it 'does not change default-src' do
- expect(subject).to include(
- "default-src https://something.test;")
- end
-
- it 'appends the proper url to frame-src CSP directives' do
- expect(subject).to include(
- "frame-src https://something.test #{observability_url} 'self'")
- end
- end
-
- context 'when frame-src and default-src exist in the CSP config' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.default_src 'https://something_default.test'
- p.frame_src 'https://something.test'
- end
- end
-
- it 'appends to frame-src CSP directives' do
- expect(subject).to include(
- "frame-src https://something.test #{observability_url} 'self'")
- expect(subject).to include(
- "default-src https://something_default.test")
- end
- end
- end
end
diff --git a/spec/requests/groups/registry/repositories_controller_spec.rb b/spec/requests/groups/registry/repositories_controller_spec.rb
index 0699f48c2be..f54acf118bb 100644
--- a/spec/requests/groups/registry/repositories_controller_spec.rb
+++ b/spec/requests/groups/registry/repositories_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Registry::RepositoriesController do
+RSpec.describe Groups::Registry::RepositoriesController, feature_category: :container_registry do
let_it_be(:group, reload: true) { create(:group) }
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/groups/settings/access_tokens_controller_spec.rb b/spec/requests/groups/settings/access_tokens_controller_spec.rb
index 6b150e0acb6..f26b69f8d30 100644
--- a/spec/requests/groups/settings/access_tokens_controller_spec.rb
+++ b/spec/requests/groups/settings/access_tokens_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Settings::AccessTokensController do
+RSpec.describe Groups::Settings::AccessTokensController, feature_category: :authentication_and_authorization do
let_it_be(:user) { create(:user) }
let_it_be(:resource) { create(:group) }
let_it_be(:access_token_user) { create(:user, :project_bot) }
diff --git a/spec/requests/groups/settings/applications_controller_spec.rb b/spec/requests/groups/settings/applications_controller_spec.rb
index 74313491414..fb91cd8bdab 100644
--- a/spec/requests/groups/settings/applications_controller_spec.rb
+++ b/spec/requests/groups/settings/applications_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Settings::ApplicationsController do
+RSpec.describe Groups::Settings::ApplicationsController, feature_category: :authentication_and_authorization do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:application) { create(:oauth_application, owner_id: group.id, owner_type: 'Namespace') }
diff --git a/spec/requests/groups/usage_quotas_controller_spec.rb b/spec/requests/groups/usage_quotas_controller_spec.rb
new file mode 100644
index 00000000000..bddc95434ce
--- /dev/null
+++ b/spec/requests/groups/usage_quotas_controller_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::UsageQuotasController, feature_category: :subscription_cost_management do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:user) { create(:user) }
+
+ subject(:request) { get group_usage_quotas_path(group) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET /groups/*group_id/-/usage_quotas' do
+ context 'when user has read_usage_quotas permission' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'renders index with 200 status code' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to match(/Placeholder for usage quotas Vue app/)
+ end
+
+ it 'renders 404 page if subgroup' do
+ get group_usage_quotas_path(subgroup)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user does not have read_usage_quotas permission' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it 'renders not_found' do
+ request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb
index 422c108f2ad..7fc14910819 100644
--- a/spec/requests/groups_controller_spec.rb
+++ b/spec/requests/groups_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GroupsController do
+RSpec.describe GroupsController, feature_category: :subgroups do
context 'token authentication' do
context 'when public group' do
let_it_be(:public_group) { create(:group, :public) }
diff --git a/spec/requests/health_controller_spec.rb b/spec/requests/health_controller_spec.rb
index ae15b63df19..83ec1565095 100644
--- a/spec/requests/health_controller_spec.rb
+++ b/spec/requests/health_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe HealthController do
+RSpec.describe HealthController, feature_category: :database do
include StubENV
let(:token) { Gitlab::CurrentSettings.health_check_access_token }
diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb
index 8d61399c824..b287ded799d 100644
--- a/spec/requests/ide_controller_spec.rb
+++ b/spec/requests/ide_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IdeController do
+RSpec.describe IdeController, feature_category: :web_ide do
using RSpec::Parameterized::TableSyntax
let_it_be(:reporter) { create(:user) }
@@ -21,7 +21,20 @@ RSpec.describe IdeController do
let(:user) { creator }
let(:branch) { '' }
+ def find_csp_frame_src
+ csp = response.headers['Content-Security-Policy']
+
+ # Transform "frame-src foo bar; connect-src foo bar; script-src ..."
+ # into array of connect-src values
+ csp.split(';')
+ .map(&:strip)
+ .find { |entry| entry.starts_with?('frame-src') }
+ .split(' ')
+ .drop(1)
+ end
+
before do
+ stub_feature_flags(vscode_web_ide: true)
sign_in(user)
end
@@ -265,5 +278,17 @@ RSpec.describe IdeController do
end
end
end
+
+ describe 'frame-src content security policy' do
+ let(:route) { '/-/ide' }
+
+ before do
+ subject
+ end
+
+ it 'adds https://*.vscode-cdn.net in frame-src CSP policy' do
+ expect(find_csp_frame_src).to include("https://*.vscode-cdn.net/")
+ end
+ end
end
end
diff --git a/spec/requests/import/github_groups_controller_spec.rb b/spec/requests/import/github_groups_controller_spec.rb
index 544cbf88cd2..6393dd35a98 100644
--- a/spec/requests/import/github_groups_controller_spec.rb
+++ b/spec/requests/import/github_groups_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::GithubGroupsController do
+RSpec.describe Import::GithubGroupsController, feature_category: :importers do
describe 'GET status' do
subject(:status) { get '/import/github_group/status', params: params, headers: headers }
diff --git a/spec/requests/import/gitlab_groups_controller_spec.rb b/spec/requests/import/gitlab_groups_controller_spec.rb
index 8d5c1e3ebab..1766c48cca1 100644
--- a/spec/requests/import/gitlab_groups_controller_spec.rb
+++ b/spec/requests/import/gitlab_groups_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::GitlabGroupsController do
+RSpec.describe Import::GitlabGroupsController, feature_category: :importers do
include WorkhorseHelpers
include_context 'workhorse headers'
diff --git a/spec/requests/import/gitlab_projects_controller_spec.rb b/spec/requests/import/gitlab_projects_controller_spec.rb
index eed035608d0..b2c2d306e53 100644
--- a/spec/requests/import/gitlab_projects_controller_spec.rb
+++ b/spec/requests/import/gitlab_projects_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::GitlabProjectsController do
+RSpec.describe Import::GitlabProjectsController, feature_category: :importers do
include WorkhorseHelpers
include_context 'workhorse headers'
diff --git a/spec/requests/import/url_controller_spec.rb b/spec/requests/import/url_controller_spec.rb
index 63af5e8b469..fa2abda6711 100644
--- a/spec/requests/import/url_controller_spec.rb
+++ b/spec/requests/import/url_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::UrlController do
+RSpec.describe Import::UrlController, feature_category: :importers do
let_it_be(:user) { create(:user) }
before do
diff --git a/spec/requests/jira_authorizations_spec.rb b/spec/requests/jira_authorizations_spec.rb
index f67288b286b..8c27b61712c 100644
--- a/spec/requests/jira_authorizations_spec.rb
+++ b/spec/requests/jira_authorizations_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Jira authorization requests' do
+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") }
diff --git a/spec/requests/jira_connect/cors_preflight_checks_controller_spec.rb b/spec/requests/jira_connect/cors_preflight_checks_controller_spec.rb
deleted file mode 100644
index d441a8575d0..00000000000
--- a/spec/requests/jira_connect/cors_preflight_checks_controller_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe JiraConnect::CorsPreflightChecksController do
- shared_examples 'allows cross-origin requests on self managed' do
- it 'renders not found' do
- options path
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(response.headers['Access-Control-Allow-Origin']).to be_nil
- end
-
- context 'with jira_connect_proxy_url setting' do
- before do
- stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
-
- options path, headers: { 'Origin' => 'http://notgitlab.com' }
- end
-
- it 'returns 200' do
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- it 'responds with access-control-allow headers', :aggregate_failures do
- expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
- expect(response.headers['Access-Control-Allow-Methods']).to eq allowed_methods
- expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
- end
-
- context 'when on GitLab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
- it 'renders not found' do
- options path
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(response.headers['Access-Control-Allow-Origin']).to be_nil
- end
- end
- end
- end
-
- describe 'OPTIONS /-/jira_connect/oauth_application_id' do
- let(:allowed_methods) { 'GET, OPTIONS' }
- let(:path) { '/-/jira_connect/oauth_application_id' }
-
- it_behaves_like 'allows cross-origin requests on self managed'
- end
-
- describe 'OPTIONS /-/jira_connect/subscriptions/:id' do
- let(:allowed_methods) { 'DELETE, OPTIONS' }
- let(:path) { '/-/jira_connect/subscriptions/123' }
-
- it_behaves_like 'allows cross-origin requests on self managed'
- end
-end
diff --git a/spec/requests/jira_connect/installations_controller_spec.rb b/spec/requests/jira_connect/installations_controller_spec.rb
index 6315c66a41a..67544bbca2e 100644
--- a/spec/requests/jira_connect/installations_controller_spec.rb
+++ b/spec/requests/jira_connect/installations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnect::InstallationsController do
+RSpec.describe JiraConnect::InstallationsController, feature_category: :integrations do
let_it_be(:installation) { create(:jira_connect_installation) }
describe 'GET /-/jira_connect/installations' do
@@ -47,16 +47,18 @@ RSpec.describe JiraConnect::InstallationsController do
end
describe 'PUT /-/jira_connect/installations' do
- before do
+ subject(:do_request) do
put '/-/jira_connect/installations', params: { jwt: jwt, installation: { instance_url: update_instance_url } }
end
- let(:update_instance_url) { 'https://example.com' }
+ let(:update_instance_url) { nil }
context 'without JWT' do
let(:jwt) { nil }
it 'returns 403' do
+ do_request
+
expect(response).to have_gitlab_http_status(:forbidden)
end
end
@@ -66,28 +68,69 @@ RSpec.describe JiraConnect::InstallationsController do
let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) }
it 'returns 200' do
+ do_request
+
expect(response).to have_gitlab_http_status(:ok)
end
- it 'updates the instance_url' do
- expect(json_response).to eq({
- 'gitlab_com' => false,
- 'instance_url' => 'https://example.com'
- })
- end
+ context 'with instance_url param' do
+ let(:update_instance_url) { 'https://example.com' }
- context 'invalid URL' do
- let(:update_instance_url) { 'invalid url' }
+ context 'instance response with success' do
+ before do
+ stub_request(:post, 'https://example.com/-/jira_connect/events/installed')
+ end
- it 'returns 422 and errors', :aggregate_failures do
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response).to eq({
- 'errors' => {
- 'instance_url' => [
- 'is blocked: Only allowed schemes are http, https'
- ]
- }
- })
+ it 'updates the instance_url' do
+ do_request
+
+ expect(json_response).to eq({
+ 'gitlab_com' => false,
+ 'instance_url' => 'https://example.com'
+ })
+ end
+
+ it 'sends an installed event to the self-managed instance' do
+ do_request
+
+ expect(WebMock).to have_requested(:post, 'https://example.com/-/jira_connect/events/installed')
+ end
+ end
+
+ context 'instance response with error' do
+ before do
+ stub_request(:post, 'https://example.com/-/jira_connect/events/installed').to_return(status: 422)
+ end
+
+ it 'returns 422 and errors', :aggregate_failures do
+ do_request
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to eq({
+ 'errors' => {
+ 'instance_url' => [
+ 'Could not be installed on the instance. Error response code 422'
+ ]
+ }
+ })
+ end
+ end
+
+ context 'invalid URL' do
+ let(:update_instance_url) { 'invalid url' }
+
+ it 'returns 422 and errors', :aggregate_failures do
+ do_request
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to eq({
+ 'errors' => {
+ 'instance_url' => [
+ 'is blocked: Only allowed schemes are http, https'
+ ]
+ }
+ })
+ end
end
end
end
diff --git a/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb b/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb
index 1d772e973ff..d111edd06da 100644
--- a/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb
+++ b/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
-RSpec.describe JiraConnect::OauthApplicationIdsController do
+RSpec.describe JiraConnect::OauthApplicationIdsController, feature_category: :integrations do
describe 'GET /-/jira_connect/oauth_application_id' do
- let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
+ let(:cors_request_headers) { { 'Origin' => 'https://gitlab.com' } }
before do
stub_application_setting(jira_connect_application_key: '123456')
@@ -38,9 +38,9 @@ RSpec.describe JiraConnect::OauthApplicationIdsController do
end
end
- context 'when jira_connect_oauth_self_managed disabled' do
+ context 'on GitLab.com' do
before do
- stub_feature_flags(jira_connect_oauth_self_managed: false)
+ allow(Gitlab).to receive(:com?).and_return(true)
end
it 'renders not found' do
@@ -49,17 +49,22 @@ RSpec.describe JiraConnect::OauthApplicationIdsController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+ end
- context 'on GitLab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
+ describe 'OPTIONS /-/jira_connect/oauth_application_id' do
+ let(:cors_request_headers) { { 'Origin' => 'https://gitlab.com', 'access-control-request-method' => 'GET' } }
- it 'renders not found' do
- get '/-/jira_connect/oauth_application_id'
+ before do
+ stub_application_setting(jira_connect_application_key: '123456')
+ stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
+ end
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ it 'allows cross-origin requests', :aggregate_failures do
+ options '/-/jira_connect/oauth_application_id', headers: cors_request_headers
+
+ expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
+ expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, OPTIONS'
+ expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
end
end
end
diff --git a/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb b/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb
index 12b9429b648..676b562c193 100644
--- a/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb
+++ b/spec/requests/jira_connect/oauth_callbacks_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnect::OauthCallbacksController do
+RSpec.describe JiraConnect::OauthCallbacksController, feature_category: :integrations do
describe 'GET /-/jira_connect/oauth_callbacks' do
context 'when logged in' do
it 'renders a page prompting the user to close the window' do
diff --git a/spec/requests/jira_connect/public_keys_controller_spec.rb b/spec/requests/jira_connect/public_keys_controller_spec.rb
index 2eca4c0ea2f..bf472469d85 100644
--- a/spec/requests/jira_connect/public_keys_controller_spec.rb
+++ b/spec/requests/jira_connect/public_keys_controller_spec.rb
@@ -2,15 +2,15 @@
require 'spec_helper'
-RSpec.describe JiraConnect::PublicKeysController do
+RSpec.describe JiraConnect::PublicKeysController, feature_category: :integrations do
describe 'GET /-/jira_connect/public_keys/:uuid' do
+ let(:uuid) { non_existing_record_id }
+ let(:public_key_storage_enabled) { true }
+
before do
- allow(Gitlab).to receive(:com?).and_return(dot_com)
+ allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage).and_return(public_key_storage_enabled)
end
- let(:uuid) { non_existing_record_id }
- let(:dot_com) { true }
-
it 'renders 404' do
get jira_connect_public_key_path(id: uuid)
@@ -29,8 +29,8 @@ RSpec.describe JiraConnect::PublicKeysController do
expect(response.body).to eq(public_key.key)
end
- context 'when not on GitLab.com' do
- let(:dot_com) { false }
+ context 'when public key storage disabled' do
+ let(:public_key_storage_enabled) { false }
it 'renders 404' do
get jira_connect_public_key_path(id: uuid)
diff --git a/spec/requests/jira_connect/subscriptions_controller_spec.rb b/spec/requests/jira_connect/subscriptions_controller_spec.rb
index b5f3ab916a4..8b019970b61 100644
--- a/spec/requests/jira_connect/subscriptions_controller_spec.rb
+++ b/spec/requests/jira_connect/subscriptions_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnect::SubscriptionsController do
+RSpec.describe JiraConnect::SubscriptionsController, feature_category: :integrations do
describe 'GET /-/jira_connect/subscriptions' do
let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'http://self-managed-gitlab.com') }
let(:qsh) do
@@ -10,7 +10,7 @@ RSpec.describe JiraConnect::SubscriptionsController do
end
let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) }
- let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
+ let(:cors_request_headers) { { 'Origin' => 'https://gitlab.com' } }
let(:path) { '/-/jira_connect/subscriptions' }
let(:params) { { jwt: jwt } }
@@ -19,7 +19,7 @@ RSpec.describe JiraConnect::SubscriptionsController do
end
subject(:content_security_policy) do
- get path, params: params
+ get path, params: params, headers: cors_request_headers
response.headers['Content-Security-Policy']
end
@@ -27,6 +27,17 @@ RSpec.describe JiraConnect::SubscriptionsController do
it { is_expected.to include('http://self-managed-gitlab.com/-/jira_connect/') }
it { is_expected.to include('http://self-managed-gitlab.com/api/') }
it { is_expected.to include('http://self-managed-gitlab.com/oauth/') }
+ it { is_expected.to include('frame-ancestors \'self\' https://*.atlassian.net https://*.jira.com') }
+
+ context 'with additional iframe ancestors' do
+ before do
+ allow(Gitlab.config.jira_connect).to receive(:additional_iframe_ancestors).and_return(['http://localhost:*', 'http://dev.gitlab.com'])
+ end
+
+ it {
+ is_expected.to include('frame-ancestors \'self\' https://*.atlassian.net https://*.jira.com http://localhost:* http://dev.gitlab.com')
+ }
+ end
context 'with no self-managed instance configured' do
let_it_be(:installation) { create(:jira_connect_installation, instance_url: '') }
@@ -36,14 +47,48 @@ RSpec.describe JiraConnect::SubscriptionsController do
it { is_expected.not_to include('http://self-managed-gitlab.com/oauth/') }
end
- context 'with jira_connect_oauth_self_managed_setting feature disabled' do
- before do
- stub_feature_flags(jira_connect_oauth_self_managed_setting: false)
+ context 'when json format' do
+ let(:path) { '/-/jira_connect/subscriptions.json' }
+
+ it 'allows cross-origin requests', :aggregate_failures do
+ get path, params: params, headers: cors_request_headers
+
+ expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
+ expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, OPTIONS'
+ expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
end
+ end
+ end
- it { is_expected.not_to include('http://self-managed-gitlab.com/-/jira_connect/') }
- it { is_expected.not_to include('http://self-managed-gitlab.com/api/') }
- it { is_expected.not_to include('http://self-managed-gitlab.com/oauth/') }
+ describe 'OPTIONS /-/jira_connect/subscriptions' do
+ let(:cors_request_headers) { { 'Origin' => 'https://gitlab.com', 'access-control-request-method' => 'GET' } }
+
+ before do
+ stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
+ end
+
+ it 'allows cross-origin requests', :aggregate_failures do
+ options '/-/jira_connect/subscriptions.json', headers: cors_request_headers
+
+ expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
+ expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, OPTIONS'
+ expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
+ end
+ end
+
+ describe 'OPTIONS /-/jira_connect/subscriptions/:id' do
+ let(:cors_request_headers) { { 'Origin' => 'https://gitlab.com', 'access-control-request-method' => 'DELETE' } }
+
+ before do
+ stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
+ end
+
+ it 'allows cross-origin requests', :aggregate_failures do
+ options '/-/jira_connect/subscriptions/1', headers: cors_request_headers
+
+ expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
+ expect(response.headers['Access-Control-Allow-Methods']).to eq 'DELETE, OPTIONS'
+ expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
end
end
@@ -56,7 +101,7 @@ RSpec.describe JiraConnect::SubscriptionsController do
end
let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) }
- let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
+ let(:cors_request_headers) { { 'Origin' => 'https://gitlab.com' } }
let(:params) { { jwt: jwt, format: :json } }
before do
diff --git a/spec/requests/jira_connect/users_controller_spec.rb b/spec/requests/jira_connect/users_controller_spec.rb
index 6e927aaba91..c02bd324708 100644
--- a/spec/requests/jira_connect/users_controller_spec.rb
+++ b/spec/requests/jira_connect/users_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnect::UsersController do
+RSpec.describe JiraConnect::UsersController, feature_category: :integrations do
describe 'GET /-/jira_connect/users' do
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/jira_routing_spec.rb b/spec/requests/jira_routing_spec.rb
index e0e170044de..f1edb58bb10 100644
--- a/spec/requests/jira_routing_spec.rb
+++ b/spec/requests/jira_routing_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Jira referenced paths', type: :request do
+RSpec.describe 'Jira referenced paths', type: :request, feature_category: :integrations do
using RSpec::Parameterized::TableSyntax
let(:user) { create(:user) }
diff --git a/spec/requests/jwks_controller_spec.rb b/spec/requests/jwks_controller_spec.rb
index c9dcc238c29..ac9765c35d8 100644
--- a/spec/requests/jwks_controller_spec.rb
+++ b/spec/requests/jwks_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JwksController do
+RSpec.describe JwksController, feature_category: :authentication_and_authorization do
describe 'Endpoints from the parent Doorkeeper::OpenidConnect::DiscoveryController' do
it 'respond successfully' do
[
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index e6916e02fde..00222cb1977 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JwtController do
+RSpec.describe JwtController, feature_category: :authentication_and_authorization do
include_context 'parsed logs'
let(:service) { double(execute: {} ) }
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 3529239a4d9..e5f03d16dda 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Git LFS API and storage' do
+RSpec.describe 'Git LFS API and storage', feature_category: :source_code_management do
using RSpec::Parameterized::TableSyntax
include LfsHttpHelpers
@@ -378,6 +378,21 @@ RSpec.describe 'Git LFS API and storage' do
it_behaves_like 'LFS http 401 response'
end
+ context 'when deploy token is from an unrelated group to the project' do
+ let(:group) { create(:group) }
+ let(:deploy_token) { create(:deploy_token, :group, groups: [group]) }
+
+ it_behaves_like 'LFS http 401 response'
+ end
+
+ context 'when deploy token is from a parent group of the project and valid' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:deploy_token) { create(:deploy_token, :group, groups: [group]) }
+
+ it_behaves_like 'an authorized request', renew_authorization: false
+ end
+
# TODO: We should fix this test case that causes flakyness by alternating the result of the above test cases.
context 'when Deploy Token is valid' do
let(:deploy_token) { create(:deploy_token, projects: [project]) }
@@ -842,14 +857,6 @@ RSpec.describe 'Git LFS API and storage' do
lfs_object.destroy!
end
- context 'with object storage disabled' do
- it "doesn't attempt to migrate file to object storage" do
- expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
-
- put_finalize(with_tempfile: true)
- end
- end
-
context 'with object storage enabled' do
context 'and direct upload enabled' do
let!(:fog_connection) do
@@ -911,18 +918,6 @@ RSpec.describe 'Git LFS API and storage' do
end
end
end
-
- context 'and background upload enabled' do
- before do
- stub_lfs_object_storage(background_upload: true)
- end
-
- it 'schedules migration of file to object storage' do
- expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('LfsObjectUploader', 'LfsObject', :file, kind_of(Numeric))
-
- put_finalize(with_tempfile: true)
- end
- end
end
end
@@ -1036,7 +1031,7 @@ RSpec.describe 'Git LFS API and storage' do
end
describe 'to a forked project' do
- let_it_be(:upstream_project) { create(:project, :public) }
+ let_it_be_with_reload(:upstream_project) { create(:project, :public) }
let_it_be(:project_owner) { create(:user) }
let(:project) { fork_project(upstream_project, project_owner) }
@@ -1074,6 +1069,56 @@ RSpec.describe 'Git LFS API and storage' do
end
end
+ describe 'when user has push access to upstream project' do
+ before do
+ upstream_project.add_maintainer(user)
+ end
+
+ context 'an MR exists on target forked project' do
+ let(:allow_collaboration) { true }
+ let(:merge_request) do
+ create(:merge_request,
+ target_project: upstream_project,
+ source_project: project,
+ allow_collaboration: allow_collaboration)
+ end
+
+ before do
+ merge_request
+ end
+
+ context 'with allow_collaboration option set to true' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ put_authorize
+ end
+
+ it_behaves_like 'LFS http 200 workhorse response'
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
+
+ it_behaves_like 'LFS http 200 response'
+ end
+ end
+
+ context 'with allow_collaboration option set to false' do
+ context 'request is sent by gitlab-workhorse to authorize the request' do
+ let(:allow_collaboration) { false }
+
+ before do
+ put_authorize
+ end
+
+ it_behaves_like 'forbidden'
+ end
+ end
+ end
+ end
+
describe 'and user does not have push access' do
it_behaves_like 'forbidden'
end
diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb
index 0eb3cb4ca07..363a16f014b 100644
--- a/spec/requests/lfs_locks_api_spec.rb
+++ b/spec/requests/lfs_locks_api_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Git LFS File Locking API' do
+RSpec.describe 'Git LFS File Locking API', feature_category: :source_code_management do
include LfsHttpHelpers
include WorkhorseHelpers
diff --git a/spec/requests/mailgun/webhooks_controller_spec.rb b/spec/requests/mailgun/webhooks_controller_spec.rb
index ae6dc89d003..669401242b2 100644
--- a/spec/requests/mailgun/webhooks_controller_spec.rb
+++ b/spec/requests/mailgun/webhooks_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mailgun::WebhooksController do
+RSpec.describe Mailgun::WebhooksController, feature_category: :team_planning do
let(:mailgun_signing_key) { 'abc123' }
let(:valid_signature) do
{
diff --git a/spec/requests/oauth/applications_controller_spec.rb b/spec/requests/oauth/applications_controller_spec.rb
index 78f0cedb56f..94ee08f6272 100644
--- a/spec/requests/oauth/applications_controller_spec.rb
+++ b/spec/requests/oauth/applications_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Oauth::ApplicationsController do
+RSpec.describe Oauth::ApplicationsController, feature_category: :authentication_and_authorization do
let_it_be(:user) { create(:user) }
let_it_be(:application) { create(:oauth_application, owner: user) }
let_it_be(:show_path) { oauth_application_path(application) }
diff --git a/spec/requests/oauth/authorizations_controller_spec.rb b/spec/requests/oauth/authorizations_controller_spec.rb
index 8d19c92865e..52188717210 100644
--- a/spec/requests/oauth/authorizations_controller_spec.rb
+++ b/spec/requests/oauth/authorizations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Oauth::AuthorizationsController do
+RSpec.describe Oauth::AuthorizationsController, feature_category: :authentication_and_authorization do
let_it_be(:user) { create(:user) }
let_it_be(:application) { create(:oauth_application, redirect_uri: 'custom://test') }
let_it_be(:oauth_authorization_path) do
diff --git a/spec/requests/oauth/tokens_controller_spec.rb b/spec/requests/oauth/tokens_controller_spec.rb
index 507489d92cf..cdfad8cb59c 100644
--- a/spec/requests/oauth/tokens_controller_spec.rb
+++ b/spec/requests/oauth/tokens_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Oauth::TokensController do
+RSpec.describe Oauth::TokensController, feature_category: :authentication_and_authorization do
let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
let(:other_headers) { {} }
let(:headers) { cors_request_headers.merge(other_headers) }
diff --git a/spec/requests/oauth_tokens_spec.rb b/spec/requests/oauth_tokens_spec.rb
index f2fb380bde0..053bd317fcc 100644
--- a/spec/requests/oauth_tokens_spec.rb
+++ b/spec/requests/oauth_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OAuth Tokens requests' do
+RSpec.describe 'OAuth Tokens requests', feature_category: :authentication_and_authorization do
let(:user) { create :user }
let(:application) { create :oauth_application, scopes: 'api' }
let(:grant_type) { 'authorization_code' }
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 3a40fec58e8..b45f4f1e39f 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OpenID Connect requests' do
+RSpec.describe 'OpenID Connect requests', feature_category: :authentication_and_authorization do
let(:user) do
create(
:user,
diff --git a/spec/requests/profiles/notifications_controller_spec.rb b/spec/requests/profiles/notifications_controller_spec.rb
index d7dfb1c675d..21e166e04d3 100644
--- a/spec/requests/profiles/notifications_controller_spec.rb
+++ b/spec/requests/profiles/notifications_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'view user notifications' do
+RSpec.describe 'view user notifications', 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/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb b/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb
index c5e7369b0a9..b0c7427fa81 100644
--- a/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb
+++ b/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects::Ci::PrometheusMetrics::HistogramsController' do
+RSpec.describe 'Projects::Ci::PrometheusMetrics::HistogramsController', feature_category: :pipeline_authoring do
let_it_be(:project) { create(:project, :public) }
describe 'POST /*namespace_id/:project_id/-/ci/prometheus_metrics/histograms' do
diff --git a/spec/requests/projects/cluster_agents_controller_spec.rb b/spec/requests/projects/cluster_agents_controller_spec.rb
index 914d5b17ba8..d7c791fa0c1 100644
--- a/spec/requests/projects/cluster_agents_controller_spec.rb
+++ b/spec/requests/projects/cluster_agents_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ClusterAgentsController do
+RSpec.describe Projects::ClusterAgentsController, feature_category: :kubernetes_management do
let_it_be(:cluster_agent) { create(:cluster_agent) }
let(:project) { cluster_agent.project }
diff --git a/spec/requests/projects/clusters/integrations_controller_spec.rb b/spec/requests/projects/clusters/integrations_controller_spec.rb
index c05e3da675c..505b63e1ff6 100644
--- a/spec/requests/projects/clusters/integrations_controller_spec.rb
+++ b/spec/requests/projects/clusters/integrations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Clusters::IntegrationsController do
+RSpec.describe Projects::Clusters::IntegrationsController, feature_category: :integrations do
include AccessMatchersForController
shared_examples 'a secure endpoint' do
diff --git a/spec/requests/projects/commits_controller_spec.rb b/spec/requests/projects/commits_controller_spec.rb
index 158902c0ffd..f9e3ef82fc1 100644
--- a/spec/requests/projects/commits_controller_spec.rb
+++ b/spec/requests/projects/commits_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::CommitsController do
+RSpec.describe Projects::CommitsController, feature_category: :source_code_management do
context 'token authentication' do
context 'when public project' do
let_it_be(:public_project) { create(:project, :repository, :public) }
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index 370febf82ff..3f9dd74c145 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'value stream analytics events' do
+RSpec.describe 'value stream analytics events', feature_category: :planning_analytics do
include CycleAnalyticsHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/projects/environments_controller_spec.rb b/spec/requests/projects/environments_controller_spec.rb
index 66ab265fc0f..41ae2d434fa 100644
--- a/spec/requests/projects/environments_controller_spec.rb
+++ b/spec/requests/projects/environments_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::EnvironmentsController do
+RSpec.describe Projects::EnvironmentsController, feature_category: :continuous_delivery do
let_it_be_with_refind(:project) { create(:project, :repository) }
let(:environment) { create(:environment, name: 'production', project: project) }
diff --git a/spec/requests/projects/google_cloud/configuration_controller_spec.rb b/spec/requests/projects/google_cloud/configuration_controller_spec.rb
index 41593b8d7a7..1aa44d1a49a 100644
--- a/spec/requests/projects/google_cloud/configuration_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/configuration_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GoogleCloud::ConfigurationController do
+RSpec.describe Projects::GoogleCloud::ConfigurationController, feature_category: :kubernetes_management do
let_it_be(:project) { create(:project, :public) }
let_it_be(:url) { project_google_cloud_configuration_path(project) }
diff --git a/spec/requests/projects/google_cloud/databases_controller_spec.rb b/spec/requests/projects/google_cloud/databases_controller_spec.rb
index 4edef71f326..e91a51ce2ef 100644
--- a/spec/requests/projects/google_cloud/databases_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/databases_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow do
+RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow, feature_category: :kubernetes_management do
shared_examples 'shared examples for database controller endpoints' do
include_examples 'requires `admin_project_google_cloud` role'
diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
index c777e8c1f69..d564a31f835 100644
--- a/spec/requests/projects/google_cloud/deployments_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GoogleCloud::DeploymentsController do
+RSpec.describe Projects::GoogleCloud::DeploymentsController, feature_category: :kubernetes_management do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:repository) { project.repository }
diff --git a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb
index e77bcdb40b8..de4b96a2e01 100644
--- a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GoogleCloud::GcpRegionsController do
+RSpec.describe Projects::GoogleCloud::GcpRegionsController, feature_category: :kubernetes_management do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:repository) { project.repository }
diff --git a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb
index 9bd8468767d..5965953cf6f 100644
--- a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GoogleCloud::RevokeOauthController do
+RSpec.describe Projects::GoogleCloud::RevokeOauthController, feature_category: :kubernetes_management do
include SessionHelpers
describe 'POST #create', :snowplow, :clean_gitlab_redis_sessions, :aggregate_failures do
diff --git a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
index d91e5a4f068..9b048f814ef 100644
--- a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
+RSpec.describe Projects::GoogleCloud::ServiceAccountsController, feature_category: :kubernetes_management do
let_it_be(:project) { create(:project, :public) }
describe 'GET index', :snowplow do
diff --git a/spec/requests/projects/harbor/artifacts_controller_spec.rb b/spec/requests/projects/harbor/artifacts_controller_spec.rb
index 310fbcf0a0f..d0ed93acaf7 100644
--- a/spec/requests/projects/harbor/artifacts_controller_spec.rb
+++ b/spec/requests/projects/harbor/artifacts_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Harbor::ArtifactsController do
+RSpec.describe Projects::Harbor::ArtifactsController, feature_category: :build_artifacts do
it_behaves_like 'a harbor artifacts controller', anonymous_status_code: '302' do
let_it_be(:container) { create(:project) }
let_it_be(:harbor_integration) { create(:harbor_integration, project: container) }
diff --git a/spec/requests/projects/harbor/repositories_controller_spec.rb b/spec/requests/projects/harbor/repositories_controller_spec.rb
index 751becaa20a..7430ac5a64f 100644
--- a/spec/requests/projects/harbor/repositories_controller_spec.rb
+++ b/spec/requests/projects/harbor/repositories_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Harbor::RepositoriesController do
+RSpec.describe Projects::Harbor::RepositoriesController, feature_category: :source_code_management do
it_behaves_like 'a harbor repositories controller', anonymous_status_code: '302' do
let_it_be(:container, reload: true) { create(:project) }
let_it_be(:harbor_integration) { create(:harbor_integration, project: container) }
diff --git a/spec/requests/projects/harbor/tags_controller_spec.rb b/spec/requests/projects/harbor/tags_controller_spec.rb
index 119d1c746ac..f1ac2f01c57 100644
--- a/spec/requests/projects/harbor/tags_controller_spec.rb
+++ b/spec/requests/projects/harbor/tags_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Harbor::TagsController do
+RSpec.describe Projects::Harbor::TagsController, feature_category: :source_code_management do
it_behaves_like 'a harbor tags controller', anonymous_status_code: '302' do
let_it_be(:container) { create(:project) }
let_it_be(:harbor_integration) { create(:harbor_integration, project: container) }
diff --git a/spec/requests/projects/hook_logs_controller_spec.rb b/spec/requests/projects/hook_logs_controller_spec.rb
index 8b3ec307e53..c71906b4895 100644
--- a/spec/requests/projects/hook_logs_controller_spec.rb
+++ b/spec/requests/projects/hook_logs_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::HookLogsController do
+RSpec.describe Projects::HookLogsController, feature_category: :integrations do
let_it_be(:user) { create(:user) }
let_it_be_with_refind(:web_hook) { create(:project_hook) }
let_it_be_with_refind(:web_hook_log) { create(:web_hook_log, web_hook: web_hook) }
diff --git a/spec/requests/projects/incident_management/pagerduty_incidents_spec.rb b/spec/requests/projects/incident_management/pagerduty_incidents_spec.rb
index 32434435475..a25dcb7f299 100644
--- a/spec/requests/projects/incident_management/pagerduty_incidents_spec.rb
+++ b/spec/requests/projects/incident_management/pagerduty_incidents_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'PagerDuty webhook' do
+RSpec.describe 'PagerDuty webhook', feature_category: :incident_management do
let_it_be(:project) { create(:project) }
describe 'POST /incidents/pagerduty' do
diff --git a/spec/requests/projects/incident_management/timeline_events_spec.rb b/spec/requests/projects/incident_management/timeline_events_spec.rb
index f7dead4834d..22a1f654ee2 100644
--- a/spec/requests/projects/incident_management/timeline_events_spec.rb
+++ b/spec/requests/projects/incident_management/timeline_events_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Timeline Events' do
+RSpec.describe 'Timeline Events', feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:incident) { create(:incident, project: project) }
diff --git a/spec/requests/projects/integrations/shimos_controller_spec.rb b/spec/requests/projects/integrations/shimos_controller_spec.rb
index 7322143f87e..bd7af0bb4ac 100644
--- a/spec/requests/projects/integrations/shimos_controller_spec.rb
+++ b/spec/requests/projects/integrations/shimos_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Projects::Integrations::ShimosController do
+RSpec.describe ::Projects::Integrations::ShimosController, feature_category: :integrations do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, developer_projects: [project]) }
let_it_be(:shimo_integration) { create(:shimo_integration, project: project) }
diff --git a/spec/requests/projects/issue_links_controller_spec.rb b/spec/requests/projects/issue_links_controller_spec.rb
index e5f40625cfa..0535156b4b8 100644
--- a/spec/requests/projects/issue_links_controller_spec.rb
+++ b/spec/requests/projects/issue_links_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::IssueLinksController do
+RSpec.describe Projects::IssueLinksController, feature_category: :team_planning do
let(:user) { create :user }
let(:project) { create(:project_empty_repo) }
let(:issue) { create :issue, project: project }
diff --git a/spec/requests/projects/issues/discussions_spec.rb b/spec/requests/projects/issues/discussions_spec.rb
index dcdca2d9c27..0f4a0bd2e5c 100644
--- a/spec/requests/projects/issues/discussions_spec.rb
+++ b/spec/requests/projects/issues/discussions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'issue discussions' do
+RSpec.describe 'issue discussions', feature_category: :team_planning do
describe 'GET /:namespace/:project/-/issues/:iid/discussions' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/projects/issues_controller_spec.rb b/spec/requests/projects/issues_controller_spec.rb
index aa2ba5e114b..bbf200eaacd 100644
--- a/spec/requests/projects/issues_controller_spec.rb
+++ b/spec/requests/projects/issues_controller_spec.rb
@@ -2,12 +2,36 @@
require 'spec_helper'
-RSpec.describe Projects::IssuesController do
+RSpec.describe Projects::IssuesController, feature_category: :team_planning do
let_it_be(:issue) { create(:issue) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { issue.project }
let_it_be(:user) { issue.author }
+ describe 'GET #new' do
+ before do
+ login_as(user)
+ end
+
+ it_behaves_like "observability csp policy", described_class do
+ let(:tested_path) do
+ new_project_issue_path(project)
+ end
+ end
+ end
+
+ describe 'GET #show' do
+ before do
+ login_as(user)
+ end
+
+ it_behaves_like "observability csp policy", described_class do
+ let(:tested_path) do
+ project_issue_path(project, issue)
+ end
+ end
+ end
+
describe 'GET #discussions' do
before do
login_as(user)
diff --git a/spec/requests/projects/merge_requests/content_spec.rb b/spec/requests/projects/merge_requests/content_spec.rb
index 7e5ec6f64c4..6c58dcb5722 100644
--- a/spec/requests/projects/merge_requests/content_spec.rb
+++ b/spec/requests/projects/merge_requests/content_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'merge request content spec' do
+RSpec.describe 'merge request content spec', feature_category: :code_review do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:merge_request) { create(:merge_request, :with_head_pipeline, target_project: project, source_project: project) }
diff --git a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
index ec65e8cf11e..10e57970704 100644
--- a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
+++ b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Requests Context Commit Diffs' do
+RSpec.describe 'Merge Requests Context Commit Diffs', feature_category: :code_review do
let_it_be(:sha1) { "33f3729a45c02fc67d00adb1b8bca394b0e761d9" }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/projects/merge_requests/creations_spec.rb b/spec/requests/projects/merge_requests/creations_spec.rb
index 842ad01656e..59e2047e1c7 100644
--- a/spec/requests/projects/merge_requests/creations_spec.rb
+++ b/spec/requests/projects/merge_requests/creations_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'merge requests creations' do
+RSpec.describe 'merge requests creations', feature_category: :code_review do
describe 'GET /:namespace/:project/merge_requests/new' do
include ProjectForksHelper
@@ -24,5 +24,17 @@ RSpec.describe 'merge requests creations' do
expect { get_new }.not_to exceed_query_limit(control)
end
+
+ it_behaves_like "observability csp policy", Projects::MergeRequests::CreationsController do
+ let(:tested_path) do
+ project_new_merge_request_path(project, merge_request: {
+ title: 'Some feature',
+ source_branch: 'fix',
+ target_branch: 'feature',
+ target_project: project,
+ source_project: project
+ })
+ end
+ end
end
end
diff --git a/spec/requests/projects/merge_requests/diffs_spec.rb b/spec/requests/projects/merge_requests/diffs_spec.rb
index 12990b54617..858acac7f0d 100644
--- a/spec/requests/projects/merge_requests/diffs_spec.rb
+++ b/spec/requests/projects/merge_requests/diffs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Merge Requests Diffs' do
+RSpec.describe 'Merge Requests Diffs', feature_category: :code_review do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -80,20 +80,6 @@ RSpec.describe 'Merge Requests Diffs' do
expect(response).to have_gitlab_http_status(:not_modified)
end
- context 'with check_etags_diffs_batch_before_write_cache flag turned off' do
- before do
- stub_feature_flags(check_etags_diffs_batch_before_write_cache: false)
- end
-
- it 'does not serialize diffs' do
- expect(PaginatedDiffSerializer).not_to receive(:new)
-
- go(headers: headers, page: 0, per_page: 5)
-
- expect(response).to have_gitlab_http_status(:not_modified)
- end
- end
-
context 'with the different user' do
let(:another_user) { create(:user) }
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
diff --git a/spec/requests/projects/merge_requests_controller_spec.rb b/spec/requests/projects/merge_requests_controller_spec.rb
index 2ee86bb423b..f5f8b5c2d83 100644
--- a/spec/requests/projects/merge_requests_controller_spec.rb
+++ b/spec/requests/projects/merge_requests_controller_spec.rb
@@ -2,11 +2,24 @@
require 'spec_helper'
-RSpec.describe Projects::MergeRequestsController do
+RSpec.describe Projects::MergeRequestsController, feature_category: :source_code_management do
+ let_it_be(:merge_request) { create(:merge_request) }
+ let_it_be(:project) { merge_request.project }
+ let_it_be(:user) { merge_request.author }
+
+ describe 'GET #show' do
+ before do
+ login_as(user)
+ end
+
+ it_behaves_like "observability csp policy", described_class do
+ let(:tested_path) do
+ project_merge_request_path(project, merge_request)
+ end
+ end
+ end
+
describe 'GET #discussions' do
- let_it_be(:merge_request) { create(:merge_request) }
- let_it_be(:project) { merge_request.project }
- let_it_be(:user) { merge_request.author }
let_it_be(:discussion) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
let_it_be(:discussion_reply) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project, in_reply_to: discussion) }
let_it_be(:state_event) { create(:resource_state_event, merge_request: merge_request) }
diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb
index 305ca6147be..d82fa284a42 100644
--- a/spec/requests/projects/merge_requests_discussions_spec.rb
+++ b/spec/requests/projects/merge_requests_discussions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'merge requests discussions' do
+RSpec.describe 'merge requests discussions', feature_category: :source_code_management do
# Further tests can be found at merge_requests_controller_spec.rb
describe 'GET /:namespace/:project/-/merge_requests/:iid/discussions' do
let(:project) { create(:project, :repository, :public) }
diff --git a/spec/requests/projects/merge_requests_spec.rb b/spec/requests/projects/merge_requests_spec.rb
index 91153554e55..9600d1a3656 100644
--- a/spec/requests/projects/merge_requests_spec.rb
+++ b/spec/requests/projects/merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'merge requests actions' do
+RSpec.describe 'merge requests actions', feature_category: :source_code_management do
let_it_be(:project) { create(:project, :repository) }
let(:merge_request) do
diff --git a/spec/requests/projects/metrics/dashboards/builder_spec.rb b/spec/requests/projects/metrics/dashboards/builder_spec.rb
index 002acca2135..c929beaed70 100644
--- a/spec/requests/projects/metrics/dashboards/builder_spec.rb
+++ b/spec/requests/projects/metrics/dashboards/builder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects::Metrics::Dashboards::BuilderController' do
+RSpec.describe 'Projects::Metrics::Dashboards::BuilderController', feature_category: :metrics do
let_it_be(:project) { create(:project) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/projects/metrics_dashboard_spec.rb b/spec/requests/projects/metrics_dashboard_spec.rb
index 61bfe1c6edf..01925f8345b 100644
--- a/spec/requests/projects/metrics_dashboard_spec.rb
+++ b/spec/requests/projects/metrics_dashboard_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects::MetricsDashboardController' do
+RSpec.describe 'Projects::MetricsDashboardController', feature_category: :metrics do
let_it_be(:project) { create(:project) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:environment2) { create(:environment, project: project) }
diff --git a/spec/requests/projects/ml/candidates_controller_spec.rb b/spec/requests/projects/ml/candidates_controller_spec.rb
new file mode 100644
index 00000000000..4a0fd1ce4f5
--- /dev/null
+++ b/spec/requests/projects/ml/candidates_controller_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.first_owner }
+ let_it_be(:experiment) { create(:ml_experiments, project: project, user: user) }
+ let_it_be(:candidate) { create(:ml_candidates, experiment: experiment, user: user) }
+
+ let(:ff_value) { true }
+ let(:threshold) { 4 }
+ let(:candidate_iid) { candidate.iid }
+
+ before do
+ stub_feature_flags(ml_experiment_tracking: false)
+ stub_feature_flags(ml_experiment_tracking: project) if ff_value
+
+ sign_in(user)
+ end
+
+ shared_examples '404 if feature flag disabled' do
+ context 'when :ml_experiment_tracking disabled' do
+ let(:ff_value) { false }
+
+ it 'is 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET show' do
+ let(:params) { basic_params.merge(id: experiment.iid) }
+
+ before do
+ show_candidate
+ end
+
+ it 'renders the template' do
+ expect(response).to render_template('projects/ml/candidates/show')
+ end
+
+ # MR removing this xit https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104166
+ xit 'does not perform N+1 sql queries' do
+ control_count = ActiveRecord::QueryRecorder.new { show_candidate }
+
+ create_list(:ml_candidate_params, 3, candidate: candidate)
+ create_list(:ml_candidate_metrics, 3, candidate: candidate)
+
+ expect { show_candidate }.not_to exceed_all_query_limit(control_count).with_threshold(threshold)
+ end
+
+ context 'when candidate does not exist' do
+ let(:candidate_iid) { non_existing_record_id.to_s }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ it_behaves_like '404 if feature flag disabled'
+ end
+
+ private
+
+ def show_candidate
+ get project_ml_candidate_path(project, candidate_iid)
+ end
+end
diff --git a/spec/requests/projects/ml/experiments_controller_spec.rb b/spec/requests/projects/ml/experiments_controller_spec.rb
index 67a2fe47dc8..f35f93b1e6c 100644
--- a/spec/requests/projects/ml/experiments_controller_spec.rb
+++ b/spec/requests/projects/ml/experiments_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Ml::ExperimentsController do
+RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
let_it_be(:project_with_feature) { create(:project, :repository) }
let_it_be(:user) { project_with_feature.first_owner }
let_it_be(:project_without_feature) do
@@ -17,7 +17,6 @@ RSpec.describe Projects::Ml::ExperimentsController do
let(:params) { basic_params }
let(:ff_value) { true }
- let(:threshold) { 4 }
let(:project) { project_with_feature }
let(:basic_params) { { namespace_id: project.namespace.to_param, project_id: project } }
@@ -48,11 +47,11 @@ RSpec.describe Projects::Ml::ExperimentsController do
end
it 'does not perform N+1 sql queries' do
- control_count = ActiveRecord::QueryRecorder.new { list_experiments }
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_experiments }
create_list(:ml_experiments, 2, project: project, user: user)
- expect { list_experiments }.not_to exceed_all_query_limit(control_count).with_threshold(threshold)
+ expect { list_experiments }.not_to exceed_all_query_limit(control_count)
end
context 'when :ml_experiment_tracking is disabled for the project' do
@@ -77,7 +76,8 @@ RSpec.describe Projects::Ml::ExperimentsController do
expect(response).to render_template('projects/ml/experiments/show')
end
- it 'does not perform N+1 sql queries' do
+ # MR removing this xit https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104166
+ xit 'does not perform N+1 sql queries' do
control_count = ActiveRecord::QueryRecorder.new { show_experiment }
create_list(:ml_candidates, 2, :with_metrics_and_params, experiment: experiment)
diff --git a/spec/requests/projects/network_controller_spec.rb b/spec/requests/projects/network_controller_spec.rb
new file mode 100644
index 00000000000..954f9655558
--- /dev/null
+++ b/spec/requests/projects/network_controller_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::NetworkController, feature_category: :source_code_management do
+ let_it_be(:project) { create(:project, :repository, :private) }
+ let(:ref) { 'master' }
+
+ describe 'GET #show' do
+ subject { get project_network_path(project, ref) }
+
+ context 'when user is unauthorized' do
+ it 'shows 404' do
+ subject
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'when user is authorized' do
+ let(:user) { project.creator }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'renders content' do
+ subject
+ expect(response).to be_successful
+ end
+
+ context 'when ref_type is provided' do
+ subject { get project_network_path(project, ref, ref_type: 'heads') }
+
+ it 'assigns url with ref_type' do
+ subject
+ expect(assigns(:url)).to eq(project_network_path(project, ref, format: :json, ref_type: 'heads'))
+ end
+
+ context 'when the use_ref_type_parameter flag is disabled' do
+ before do
+ stub_feature_flags(use_ref_type_parameter: false)
+ end
+
+ it 'assigns url without ref_type' do
+ subject
+ expect(assigns(:url)).to eq(project_network_path(project, ref, format: :json))
+ end
+ end
+ end
+
+ it 'assigns url' do
+ subject
+ expect(assigns(:url)).to eq(project_network_path(project, ref, format: :json))
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/noteable_notes_spec.rb b/spec/requests/projects/noteable_notes_spec.rb
index 44ee50ca002..5699bf17b80 100644
--- a/spec/requests/projects/noteable_notes_spec.rb
+++ b/spec/requests/projects/noteable_notes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project noteable notes' do
+RSpec.describe 'Project noteable notes', feature_category: :team_planning do
describe '#index' do
let_it_be(:merge_request) { create(:merge_request) }
diff --git a/spec/requests/projects/packages/package_files_controller_spec.rb b/spec/requests/projects/packages/package_files_controller_spec.rb
index a6daf57f0fa..e5849be9f13 100644
--- a/spec/requests/projects/packages/package_files_controller_spec.rb
+++ b/spec/requests/projects/packages/package_files_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Packages::PackageFilesController do
+RSpec.describe Projects::Packages::PackageFilesController, feature_category: :package_registry do
let_it_be(:project) { create(:project, :public) }
let_it_be(:package) { create(:package, project: project) }
let_it_be(:package_file) { create(:package_file, package: package) }
diff --git a/spec/requests/projects/pipelines_controller_spec.rb b/spec/requests/projects/pipelines_controller_spec.rb
index 1c6b1039aee..7f185ade339 100644
--- a/spec/requests/projects/pipelines_controller_spec.rb
+++ b/spec/requests/projects/pipelines_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::PipelinesController do
+RSpec.describe Projects::PipelinesController, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/requests/projects/redirect_controller_spec.rb b/spec/requests/projects/redirect_controller_spec.rb
index 3bbca3ca32b..e828c546198 100644
--- a/spec/requests/projects/redirect_controller_spec.rb
+++ b/spec/requests/projects/redirect_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Projects::RedirectController requests" do
+RSpec.describe "Projects::RedirectController requests", feature_category: :projects do
using RSpec::Parameterized::TableSyntax
let_it_be(:private_project) { create(:project, :private) }
diff --git a/spec/requests/projects/releases_controller_spec.rb b/spec/requests/projects/releases_controller_spec.rb
index 8e492125ace..d331142583d 100644
--- a/spec/requests/projects/releases_controller_spec.rb
+++ b/spec/requests/projects/releases_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects::ReleasesController' do
+RSpec.describe 'Projects::ReleasesController', feature_category: :release_orchestration do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/projects/settings/access_tokens_controller_spec.rb b/spec/requests/projects/settings/access_tokens_controller_spec.rb
index 17389cdcce7..defb35fd496 100644
--- a/spec/requests/projects/settings/access_tokens_controller_spec.rb
+++ b/spec/requests/projects/settings/access_tokens_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Settings::AccessTokensController do
+RSpec.describe Projects::Settings::AccessTokensController, feature_category: :authentication_and_authorization do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:resource) { create(:project, group: group) }
diff --git a/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb b/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb
index 77daff901a1..6cd0df19468 100644
--- a/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb
+++ b/spec/requests/projects/settings/integration_hook_logs_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Settings::IntegrationHookLogsController do
+RSpec.describe Projects::Settings::IntegrationHookLogsController, feature_category: :integrations do
let_it_be(:user) { create(:user) }
let_it_be(:integration) { create(:datadog_integration) }
let_it_be_with_refind(:web_hook) { integration.service_hook }
diff --git a/spec/requests/projects/settings/packages_and_registries_controller_spec.rb b/spec/requests/projects/settings/packages_and_registries_controller_spec.rb
index 6d8a152c769..2806beadd4e 100644
--- a/spec/requests/projects/settings/packages_and_registries_controller_spec.rb
+++ b/spec/requests/projects/settings/packages_and_registries_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Settings::PackagesAndRegistriesController do
+RSpec.describe Projects::Settings::PackagesAndRegistriesController, feature_category: :package_registry do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) }
diff --git a/spec/requests/projects/tags_controller_spec.rb b/spec/requests/projects/tags_controller_spec.rb
index b9531a2739c..c0b0b1728c2 100644
--- a/spec/requests/projects/tags_controller_spec.rb
+++ b/spec/requests/projects/tags_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::TagsController do
+RSpec.describe Projects::TagsController, feature_category: :source_code_management do
context 'token authentication' do
context 'when public project' do
let_it_be(:public_project) { create(:project, :repository, :public) }
diff --git a/spec/requests/projects/uploads_spec.rb b/spec/requests/projects/uploads_spec.rb
index de5ef36be7e..aec2636b69c 100644
--- a/spec/requests/projects/uploads_spec.rb
+++ b/spec/requests/projects/uploads_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'File uploads' do
+RSpec.describe 'File uploads', feature_category: :not_owned do
include WorkhorseHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/requests/projects/usage_quotas_spec.rb b/spec/requests/projects/usage_quotas_spec.rb
index 6e449a21804..60ab64c30c3 100644
--- a/spec/requests/projects/usage_quotas_spec.rb
+++ b/spec/requests/projects/usage_quotas_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Usage Quotas' do
+RSpec.describe 'Project Usage Quotas', feature_category: :subscription_cost_management do
let_it_be(:project) { create(:project) }
let_it_be(:role) { :maintainer }
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/projects/work_items_spec.rb b/spec/requests/projects/work_items_spec.rb
index 4d7acc73d4f..056416d380d 100644
--- a/spec/requests/projects/work_items_spec.rb
+++ b/spec/requests/projects/work_items_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Work Items' do
+RSpec.describe 'Work Items', feature_category: :team_planning do
let_it_be(:work_item) { create(:work_item) }
let_it_be(:developer) { create(:user) }
diff --git a/spec/requests/projects_controller_spec.rb b/spec/requests/projects_controller_spec.rb
index d2200d5a4ec..f08f3578dc0 100644
--- a/spec/requests/projects_controller_spec.rb
+++ b/spec/requests/projects_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectsController do
+RSpec.describe ProjectsController, feature_category: :projects do
context 'token authentication' do
context 'when public project' do
let_it_be(:public_project) { create(:project, :public) }
diff --git a/spec/requests/pwa_controller_spec.rb b/spec/requests/pwa_controller_spec.rb
index 7a295b17231..3971790c094 100644
--- a/spec/requests/pwa_controller_spec.rb
+++ b/spec/requests/pwa_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PwaController do
+RSpec.describe PwaController, feature_category: :navigation do
describe 'GET #manifest' do
it 'responds with json' do
get manifest_path(format: :json)
@@ -10,6 +10,23 @@ RSpec.describe PwaController do
expect(response.body).to include('The complete DevOps platform.')
expect(response).to have_gitlab_http_status(:success)
end
+
+ context 'with customized appearance' do
+ let_it_be(:appearance) do
+ create(:appearance, title: 'Long name', short_title: 'Short name', description: 'This is a test')
+ end
+
+ it 'uses custom values', :aggregate_failures do
+ get manifest_path(format: :json)
+
+ expect(Gitlab::Json.parse(response.body)).to include({
+ 'description' => 'This is a test',
+ 'name' => 'Long name',
+ 'short_name' => 'Short name'
+ })
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
end
describe 'GET #offline' do
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index f6b9bc527ac..643a98da441 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_caching do
+RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_caching,
+feature_category: :authentication_and_authorization do
include RackAttackSpecHelpers
include SessionHelpers
diff --git a/spec/requests/recursive_webhook_detection_spec.rb b/spec/requests/recursive_webhook_detection_spec.rb
index fe27c90b6c8..a74d4f9a603 100644
--- a/spec/requests/recursive_webhook_detection_spec.rb
+++ b/spec/requests/recursive_webhook_detection_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_redis_shared_state, :request_store do
+RSpec.describe 'Recursive webhook detection', :sidekiq_inline, :clean_gitlab_redis_shared_state, :request_store,
+feature_category: :integrations do
include StubRequests
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/robots_txt_spec.rb b/spec/requests/robots_txt_spec.rb
index 7c0b7d8117a..18a14677e0c 100644
--- a/spec/requests/robots_txt_spec.rb
+++ b/spec/requests/robots_txt_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Robots.txt Requests', :aggregate_failures do
+RSpec.describe 'Robots.txt Requests', :aggregate_failures, feature_category: :build do
before do
Gitlab::Testing::RobotsBlockerMiddleware.block_requests!
end
diff --git a/spec/requests/runner_setup_controller_spec.rb b/spec/requests/runner_setup_controller_spec.rb
index 665c896e30d..8d75b9e81b7 100644
--- a/spec/requests/runner_setup_controller_spec.rb
+++ b/spec/requests/runner_setup_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RunnerSetupController do
+RSpec.describe RunnerSetupController, feature_category: :runner_fleet do
let(:user) { create(:user) }
before do
diff --git a/spec/requests/sandbox_controller_spec.rb b/spec/requests/sandbox_controller_spec.rb
index 4fc26580123..77913065380 100644
--- a/spec/requests/sandbox_controller_spec.rb
+++ b/spec/requests/sandbox_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SandboxController do
+RSpec.describe SandboxController, feature_category: :not_owned do
describe 'GET #mermaid' do
it 'renders page without template' do
get sandbox_mermaid_path
diff --git a/spec/requests/search_controller_spec.rb b/spec/requests/search_controller_spec.rb
index 613732c19ea..98dda75a2b0 100644
--- a/spec/requests/search_controller_spec.rb
+++ b/spec/requests/search_controller_spec.rb
@@ -2,10 +2,11 @@
require 'spec_helper'
-RSpec.describe SearchController, type: :request do
+RSpec.describe SearchController, type: :request, feature_category: :global_search do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :public, :repository, :wiki_repo, name: 'awesome project', group: group) }
+ let_it_be(:projects) { create_list(:project, 5, :public, :repository, :wiki_repo) }
before do
login_as(user)
@@ -20,9 +21,16 @@ RSpec.describe SearchController, type: :request do
create(object, *creation_traits, creation_args)
control = ActiveRecord::QueryRecorder.new(skip_cached: false) { send_search_request(params) }
- create_list(object, 3, *creation_traits, creation_args)
+ expect(response.body).to include('search-results') # Confirm there are search results to prevent false positives
+
+ projects.each do |project|
+ creation_args[:source_project] = project if creation_args.key?(:source_project)
+ creation_args[:project] = project if creation_args.key?(:project)
+ create(object, *creation_traits, creation_args)
+ end
expect { send_search_request(params) }.not_to exceed_all_query_limit(control).with_threshold(threshold)
+ expect(response.body).to include('search-results') # Confirm there are search results to prevent false positives
end
end
@@ -33,26 +41,26 @@ RSpec.describe SearchController, type: :request do
let(:object) { :issue }
let(:creation_args) { { project: project, title: 'foo' } }
let(:params) { { search: 'foo', scope: 'issues' } }
- # there are 4 additional queries run for the logged in user:
- # (1) geo_nodes, (1) users, (2) broadcast_messages
- let(:threshold) { 4 }
+ # some N+1 queries still exist
+ # each issue runs an extra query for group namespaces
+ let(:threshold) { 1 }
it_behaves_like 'an efficient database result'
end
- context 'for merge_request scope' do
+ context 'for merge_requests scope' do
let(:creation_traits) { [:unique_branches] }
let(:object) { :merge_request }
let(:creation_args) { { source_project: project, title: 'bar' } }
let(:params) { { search: 'bar', scope: 'merge_requests' } }
- # there are 4 additional queries run for the logged in user:
- # - (1) geo_nodes, (1) users, (2) broadcast_messages
+ # some N+1 queries still exist
+ # each merge request runs an extra query for project routes
let(:threshold) { 4 }
it_behaves_like 'an efficient database result'
end
- context 'for project scope' do
+ context 'for projects scope' do
let(:creation_traits) { [:public] }
let(:object) { :project }
let(:creation_args) { { name: 'project' } }
@@ -63,12 +71,76 @@ RSpec.describe SearchController, type: :request do
# - one count for open MRs
# - one count for open Issues
# there are 4 additional queries run for the logged in user:
- # (1) geo_nodes, (1) users, (2) broadcast_messages
- let(:threshold) { 13 }
+ # (1) user preferences, (1) user statuses, (1) user details, (1) users
+ let(:threshold) { 17 }
it_behaves_like 'an efficient database result'
end
+ context 'for milestones scope' do
+ let(:object) { :milestone }
+ let(:creation_args) { { project: project } }
+ let(:params) { { search: 'title', scope: 'milestones' } }
+ let(:threshold) { 0 }
+
+ it_behaves_like 'an efficient database result'
+ end
+
+ context 'for users scope' do
+ let(:object) { :user }
+ let(:creation_args) { { name: 'georgia' } }
+ let(:params) { { search: 'georgia', scope: 'users' } }
+ let(:threshold) { 0 }
+
+ it_behaves_like 'an efficient database result'
+ end
+
+ context 'for notes scope' do
+ let(:creation_traits) { [:on_commit] }
+ let(:object) { :note }
+ let(:creation_args) { { project: project, note: 'hello world' } }
+ let(:params) { { search: 'hello world', scope: 'notes', project_id: project.id } }
+ let(:threshold) { 0 }
+
+ it_behaves_like 'an efficient database result'
+ end
+
+ context 'for blobs scope' do
+ # blobs are enabled for project search only in basic search
+ let(:params_for_one) { { search: 'test', project_id: project.id, scope: 'blobs', per_page: 1 } }
+ let(:params_for_many) { { search: 'test', project_id: project.id, scope: 'blobs', per_page: 5 } }
+
+ it 'avoids N+1 database queries' do
+ control = ActiveRecord::QueryRecorder.new { send_search_request(params_for_one) }
+ expect(response.body).to include('search-results') # Confirm search results to prevent false positives
+
+ expect { send_search_request(params_for_many) }.not_to exceed_query_limit(control.count)
+ expect(response.body).to include('search-results') # Confirm search results to prevent false positives
+ end
+ end
+
+ context 'for commits scope' do
+ let(:params_for_one) { { search: 'test', project_id: project.id, scope: 'commits', per_page: 1 } }
+ let(:params_for_many) { { search: 'test', project_id: project.id, scope: 'commits', per_page: 5 } }
+
+ it 'avoids N+1 database queries' do
+ control = ActiveRecord::QueryRecorder.new { send_search_request(params_for_one) }
+ expect(response.body).to include('search-results') # Confirm search results to prevent false positives
+
+ expect { send_search_request(params_for_many) }.not_to exceed_query_limit(control.count)
+ expect(response.body).to include('search-results') # Confirm search results to prevent false positives
+ end
+ end
+
+ context 'for code search' do
+ let(:params_for_code_search) { { search: 'blob: hello' } }
+
+ it 'sets scope to blobs if code search literals are used' do
+ send_search_request(params_for_code_search)
+ expect(response).to redirect_to(search_path(params_for_code_search.merge({ scope: 'blobs' })))
+ end
+ end
+
context 'when searching by SHA' do
let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
diff --git a/spec/requests/self_monitoring_project_spec.rb b/spec/requests/self_monitoring_project_spec.rb
index 64c5f94657d..ce4dd10a52d 100644
--- a/spec/requests/self_monitoring_project_spec.rb
+++ b/spec/requests/self_monitoring_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Self-Monitoring project requests' do
+RSpec.describe 'Self-Monitoring project requests', feature_category: :projects do
let(:admin) { create(:admin) }
describe 'POST #create_self_monitoring_project' do
diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb
index 95df181b7b0..7b3fd23980a 100644
--- a/spec/requests/sessions_spec.rb
+++ b/spec/requests/sessions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Sessions' do
+RSpec.describe 'Sessions', feature_category: :authentication_and_authorization do
context 'authentication', :allow_forgery_protection do
let(:user) { create(:user) }
diff --git a/spec/requests/terraform/services_controller_spec.rb b/spec/requests/terraform/services_controller_spec.rb
index 54f7348513e..928c57613fa 100644
--- a/spec/requests/terraform/services_controller_spec.rb
+++ b/spec/requests/terraform/services_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Terraform::ServicesController do
+RSpec.describe Terraform::ServicesController, feature_category: :package_registry do
describe 'GET /.well-known/terraform.json' do
subject { get '/.well-known/terraform.json' }
diff --git a/spec/requests/user_activity_spec.rb b/spec/requests/user_activity_spec.rb
index 148bb2d6fae..f9682d81640 100644
--- a/spec/requests/user_activity_spec.rb
+++ b/spec/requests/user_activity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Update of user activity' do
+RSpec.describe 'Update of user activity', feature_category: :users do
paths_to_visit = [
'/group',
'/group/project',
diff --git a/spec/requests/user_avatar_spec.rb b/spec/requests/user_avatar_spec.rb
index 1397741af18..4e3c2744d56 100644
--- a/spec/requests/user_avatar_spec.rb
+++ b/spec/requests/user_avatar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Loading a user avatar' do
+RSpec.describe 'Loading a user avatar', feature_category: :users do
let(:user) { create(:user, :with_avatar) }
context 'when logged in' do
diff --git a/spec/requests/user_sends_malformed_strings_spec.rb b/spec/requests/user_sends_malformed_strings_spec.rb
index da533606be5..4c131bfe452 100644
--- a/spec/requests/user_sends_malformed_strings_spec.rb
+++ b/spec/requests/user_sends_malformed_strings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User sends malformed strings' do
+RSpec.describe 'User sends malformed strings', feature_category: :user_management do
include GitHttpHelpers
let(:null_byte) { "\u0000" }
diff --git a/spec/requests/user_spoofs_ip_spec.rb b/spec/requests/user_spoofs_ip_spec.rb
index 833dae78529..0244d60bc3b 100644
--- a/spec/requests/user_spoofs_ip_spec.rb
+++ b/spec/requests/user_spoofs_ip_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User spoofs their IP' do
+RSpec.describe 'User spoofs their IP', feature_category: :user_management do
it 'raises a 400 error' do
get '/nonexistent', headers: { 'Client-Ip' => '1.2.3.4', 'X-Forwarded-For' => '5.6.7.8' }
diff --git a/spec/requests/users/group_callouts_spec.rb b/spec/requests/users/group_callouts_spec.rb
index a8680c3add4..d186bf92bc7 100644
--- a/spec/requests/users/group_callouts_spec.rb
+++ b/spec/requests/users/group_callouts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group callouts' do
+RSpec.describe 'Group callouts', feature_category: :navigation do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/users/project_callouts_spec.rb b/spec/requests/users/project_callouts_spec.rb
index 98c00fef052..a15dd225e84 100644
--- a/spec/requests/users/project_callouts_spec.rb
+++ b/spec/requests/users/project_callouts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project callouts' do
+RSpec.describe 'Project callouts', feature_category: :navigation do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index e78d4cc326e..608284c05f3 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UsersController do
+RSpec.describe UsersController, feature_category: :user_management do
# This user should have the same e-mail address associated with the GPG key prepared for tests
let(:user) { create(:user, email: GpgHelpers::User1.emails[0]) }
let(:private_user) { create(:user, private_profile: true) }
diff --git a/spec/requests/verifies_with_email_spec.rb b/spec/requests/verifies_with_email_spec.rb
index 34fda1cce4d..cac754a9cb1 100644
--- a/spec/requests/verifies_with_email_spec.rb
+++ b/spec/requests/verifies_with_email_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_redis_rate_limiting do
+RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_redis_rate_limiting,
+feature_category: :user_management do
include SessionHelpers
include EmailHelpers
@@ -78,15 +79,25 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
end
context 'when the user is signing in from an unknown ip address' do
+ let(:ip_check_enabled) { true }
+
before do
+ stub_feature_flags(check_ip_address_for_email_verification: ip_check_enabled)
allow(AuthenticationEvent)
.to receive(:initial_login_or_known_ip_address?)
.and_return(false)
+
perform_enqueued_jobs { sign_in }
end
it_behaves_like 'send verification instructions'
it_behaves_like 'prompt for email verification'
+
+ context 'when the check_ip_address_for_email_verification feature flag is disabled' do
+ let(:ip_check_enabled) { false }
+
+ it_behaves_like 'not verifying with email'
+ end
end
end
@@ -187,6 +198,18 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
expect(response).to redirect_to(users_successful_verification_path)
end
end
+
+ context 'when not completing identity verification and logging in with another account' do
+ let(:another_user) { create(:user) }
+
+ before do
+ post user_session_path, params: { user: { login: another_user.username, password: another_user.password } }
+ end
+
+ it 'does not redirect to the successful verification path' do
+ expect(response).not_to redirect_to(users_successful_verification_path)
+ end
+ end
end
context 'when signing in with a valid password' do
diff --git a/spec/requests/web_ide/remote_ide_controller_spec.rb b/spec/requests/web_ide/remote_ide_controller_spec.rb
new file mode 100644
index 00000000000..367c7527f10
--- /dev/null
+++ b/spec/requests/web_ide/remote_ide_controller_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_development do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:top_nav_partial) { 'layouts/header/_default' }
+
+ let_it_be(:connection_token) { 'random1Connection3Token7' }
+ let_it_be(:remote_path) { 'test/foo/README.md' }
+ let_it_be(:return_url) { 'https://example.com/-/original/location' }
+ let_it_be(:csp_nonce) { 'just=some=noncense' }
+
+ let(:remote_host) { 'my-remote-host.example.com:1234' }
+ let(:ff_vscode_web_ide) { true }
+
+ before do
+ sign_in(user)
+
+ stub_feature_flags(vscode_web_ide: ff_vscode_web_ide)
+
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:content_security_policy_nonce).and_return(csp_nonce)
+ end
+ end
+
+ shared_examples_for '404 response' do
+ it 'has not_found status' do
+ post_to_remote_ide
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe "#index" do
+ context "when feature flag is on *and* user is not using legacy Web IDE" do
+ before do
+ post_to_remote_ide
+ end
+
+ it "renders the correct layout" do
+ expect(response).to render_template(layout: 'fullscreen')
+ end
+
+ it "renders with minimal: true" do
+ # This indirectly tests that `minimal: true` was passed to the fullscreen layout
+ expect(response).not_to render_template(top_nav_partial)
+ end
+
+ it "renders root element with data" do
+ expected = {
+ connection_token: connection_token,
+ remote_host: remote_host,
+ remote_path: remote_path,
+ return_url: return_url,
+ csp_nonce: csp_nonce
+ }
+
+ expect(find_root_element_data).to eq(expected)
+ end
+
+ it "updates the content security policy with the correct connect sources" do
+ expect(find_csp_source('connect-src')).to include(
+ "ws://#{remote_host}",
+ "wss://#{remote_host}",
+ "http://#{remote_host}",
+ "https://#{remote_host}"
+ )
+ end
+
+ it "updates the content security policy with the correct frame sources" do
+ expect(find_csp_source('frame-src')).to include("https://*.vscode-cdn.net/")
+ end
+ end
+
+ context 'when remote_host does not have port' do
+ let(:remote_host) { "my-remote-host.example.com" }
+
+ before do
+ post_to_remote_ide
+ end
+
+ it "updates the content security policy with the correct remote_host" do
+ expect(find_csp_source('connect-src')).to include(
+ "ws://#{remote_host}",
+ "wss://#{remote_host}",
+ "http://#{remote_host}",
+ "https://#{remote_host}"
+ )
+ end
+
+ it 'renders remote_host in root element data' do
+ expect(find_root_element_data).to include(remote_host: remote_host)
+ end
+ end
+
+ context 'when feature flag is off' do
+ let(:ff_vscode_web_ide) { false }
+
+ it_behaves_like '404 response'
+ end
+
+ context "when the remote host is invalid" do
+ let(:remote_host) { 'invalid:host:1:1:' }
+
+ it_behaves_like '404 response'
+ end
+ end
+
+ def find_root_element_data
+ ide_attrs = Nokogiri::HTML.parse(response.body).at_css('#ide').attributes.transform_values(&:value)
+
+ {
+ connection_token: ide_attrs['data-connection-token'],
+ remote_host: ide_attrs['data-remote-host'],
+ remote_path: ide_attrs['data-remote-path'],
+ return_url: ide_attrs['data-return-url'],
+ csp_nonce: ide_attrs['data-csp-nonce']
+ }
+ end
+
+ def find_csp_source(key)
+ csp = response.headers['Content-Security-Policy']
+
+ # Transform "default-src foo bar; connect-src foo bar; script-src ..."
+ # into array of values for a single directive based on the given key
+ csp.split(';')
+ .map(&:strip)
+ .find { |entry| entry.starts_with?(key) }
+ .split(' ')
+ .drop(1)
+ end
+
+ def post_to_remote_ide
+ params = {
+ connection_token: connection_token,
+ return_url: return_url
+ }
+
+ post ide_remote_path(remote_host: remote_host, remote_path: remote_path), params: params
+ end
+end
diff --git a/spec/requests/whats_new_controller_spec.rb b/spec/requests/whats_new_controller_spec.rb
index d4976a2bba3..1e8cb514945 100644
--- a/spec/requests/whats_new_controller_spec.rb
+++ b/spec/requests/whats_new_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WhatsNewController, :clean_gitlab_redis_cache do
+RSpec.describe WhatsNewController, :clean_gitlab_redis_cache, feature_category: :navigation do
after do
ReleaseHighlight.instance_variable_set(:@file_paths, nil)
end
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index 68e619e5246..54fbe9e962d 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -83,6 +83,10 @@ RSpec.shared_examples 'groups routing' do
it 'routes to the observability controller manage method' do
expect(get("groups/#{group_path}/-/observability/manage")).to route_to('groups/observability#manage', group_id: group_path)
end
+
+ it 'routes to the usage quotas controller' do
+ expect(get("groups/#{group_path}/-/usage_quotas")).to route_to("groups/usage_quotas#index", group_id: group_path)
+ end
end
RSpec.describe "Groups", "routing" do
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 42196a7d8af..1b6a5182531 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -240,7 +240,7 @@ RSpec.describe 'project routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/-/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff')
expect(get('/gitlab/gitlabhq/-/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch')
- expect(get('/gitlab/gitlabhq/-/merge_requests/1/diffs')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'diffs')
+ expect(get('/gitlab/gitlabhq/-/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'diffs')
expect(get('/gitlab/gitlabhq/-/merge_requests/1/commits')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'commits')
expect(get('/gitlab/gitlabhq/-/merge_requests/1/pipelines')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'pipelines')
end
@@ -248,7 +248,7 @@ RSpec.describe 'project routing' do
it 'to #show from scoped route' do
expect(get('/gitlab/gitlabhq/-/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff')
expect(get('/gitlab/gitlabhq/-/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch')
- expect(get('/gitlab/gitlabhq/-/merge_requests/1/diffs')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'diffs')
+ expect(get('/gitlab/gitlabhq/-/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', tab: 'diffs')
end
it_behaves_like 'resource routing' do
@@ -301,6 +301,7 @@ RSpec.describe 'project routing' do
expect(get('/gitlab/gitlabhq/-/merge_requests/1/conflicts')).to route_to('projects/merge_requests/conflicts#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
end
end
+
# raw_project_snippet GET /:project_id/snippets/:id/raw(.:format) snippets#raw
# project_snippets GET /:project_id/snippets(.:format) snippets#index
# new_project_snippet GET /:project_id/snippets/new(.:format) snippets#new
diff --git a/spec/routing/user_routing_spec.rb b/spec/routing/user_routing_spec.rb
new file mode 100644
index 00000000000..7bb589565fa
--- /dev/null
+++ b/spec/routing/user_routing_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'user routing', :clean_gitlab_redis_sessions, feature_category: :authentication_and_authorization do
+ include SessionHelpers
+
+ context 'when GitHub OAuth on project import is cancelled' do
+ it_behaves_like 'redirecting a legacy path', '/users/auth?error=access_denied&state=xyz', '/users/sign_in'
+ end
+
+ context 'when GitHub OAuth on sign in is cancelled' do
+ before do
+ stub_session(auth_on_failure_path: '/projects/new#import_project')
+ end
+
+ context 'when all required parameters are present' do
+ it_behaves_like 'redirecting a legacy path',
+ '/users/auth?error=access_denied&state=xyz',
+ '/projects/new#import_project'
+ end
+
+ context 'when one of the required parameters is missing' do
+ it_behaves_like 'redirecting a legacy path',
+ '/users/auth?error=access_denied&state=',
+ '/auth'
+ end
+ end
+end
diff --git a/spec/routing/web_ide_routing_spec.rb b/spec/routing/web_ide_routing_spec.rb
new file mode 100644
index 00000000000..58c24189dfd
--- /dev/null
+++ b/spec/routing/web_ide_routing_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe "Web IDE routing", feature_category: :remote_development do
+ describe 'remote' do
+ it "routes to #index, without remote_path" do
+ expect(post("/-/ide/remote/my.env.gitlab.example.com%3A3443")).to route_to(
+ "web_ide/remote_ide#index",
+ remote_host: 'my.env.gitlab.example.com:3443'
+ )
+ end
+
+ it "routes to #index, with remote_path" do
+ expect(post("/-/ide/remote/my.env.gitlab.example.com%3A3443/foo/bar.dev/test.dir")).to route_to(
+ "web_ide/remote_ide#index",
+ remote_host: 'my.env.gitlab.example.com:3443',
+ remote_path: 'foo/bar.dev/test.dir'
+ )
+ end
+ end
+end
diff --git a/spec/rubocop/cop/feature_flag_usage_spec.rb b/spec/rubocop/cop/feature_flag_usage_spec.rb
new file mode 100644
index 00000000000..13f58ca7084
--- /dev/null
+++ b/spec/rubocop/cop/feature_flag_usage_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../rubocop/cop/feature_flag_usage'
+
+RSpec.describe RuboCop::Cop::FeatureFlagUsage, feature_category: :scalability do
+ let(:msg) { described_class::MSG }
+
+ context 'when calling Feature.enabled?' do
+ it 'registers offence' do
+ expect_offense(<<~PATTERN)
+ Feature.enabled?(:fflag)
+ ^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
+ PATTERN
+ end
+
+ it 'registers offence when called with type parameter' do
+ expect_offense(<<~PATTERN)
+ Feature.enabled?(:fflag, type: :ops)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
+ PATTERN
+ end
+
+ it 'registers offence when called under global namespace' do
+ expect_offense(<<~PATTERN)
+ ::Feature.enabled?(:fflag, type: :ops)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
+ PATTERN
+ end
+ end
+
+ context 'when calling Feature.disabled?' do
+ it 'registers offence' do
+ expect_offense(<<~PATTERN)
+ Feature.disabled?(:fflag)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
+ PATTERN
+ end
+
+ it 'registers offence when called with type parameter' do
+ expect_offense(<<~PATTERN)
+ Feature.disabled?(:fflag, type: :ops)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
+ PATTERN
+ end
+
+ it 'registers offence when called under global namespace' do
+ expect_offense(<<~PATTERN)
+ ::Feature.disabled?(:fflag, type: :ops)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
+ PATTERN
+ end
+ end
+end
diff --git a/spec/rubocop/cop/filename_length_spec.rb b/spec/rubocop/cop/filename_length_spec.rb
index 1ea368d282f..a5bdce9a339 100644
--- a/spec/rubocop/cop/filename_length_spec.rb
+++ b/spec/rubocop/cop/filename_length_spec.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rubocop_spec_helper'
-require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/filename_length'
RSpec.describe RuboCop::Cop::FilenameLength do
diff --git a/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb b/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb
index 30edd33a318..b15c298099d 100644
--- a/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb
+++ b/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
require 'rubocop_spec_helper'
-require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/feature_available_usage'
RSpec.describe RuboCop::Cop::Gitlab::FeatureAvailableUsage 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 6e60889f737..bfc0cebe203 100644
--- a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
+++ b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
require 'rubocop_spec_helper'
-require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/mark_used_feature_flags'
RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
diff --git a/spec/rubocop/cop/gitlab/strong_memoize_attr_spec.rb b/spec/rubocop/cop/gitlab/strong_memoize_attr_spec.rb
new file mode 100644
index 00000000000..0ed699f4e8c
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/strong_memoize_attr_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/gitlab/strong_memoize_attr'
+
+RSpec.describe RuboCop::Cop::Gitlab::StrongMemoizeAttr do
+ context 'when strong_memoize() is the entire body of a method' do
+ context 'when the memoization name is the same as the method name' do
+ it 'registers an offense and autocorrects' do
+ expect_offense(<<~RUBY)
+ class Foo
+ def memoized_method
+ strong_memoize(:memoized_method) do
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `strong_memoize_attr`, instead of using `strong_memoize` directly
+ 'This is a memoized method'
+ end
+ end
+ end
+ RUBY
+
+ expect_correction(<<~RUBY)
+ class Foo
+ def memoized_method
+ 'This is a memoized method'
+ end
+ strong_memoize_attr :memoized_method
+ end
+ RUBY
+ end
+ end
+
+ context 'when the memoization name is different from the method name' do
+ it 'registers an offense and autocorrects' do
+ expect_offense(<<~RUBY)
+ class Foo
+ def enabled?
+ strong_memoize(:enabled) do
+ ^^^^^^^^^^^^^^^^^^^^^^^^ Use `strong_memoize_attr`, instead of using `strong_memoize` directly
+ true
+ end
+ end
+ end
+ RUBY
+
+ expect_correction(<<~RUBY)
+ class Foo
+ def enabled?
+ true
+ end
+ strong_memoize_attr :enabled?, :enabled
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when strong_memoize() is not the entire body of the method' do
+ it 'registers an offense and does not autocorrect' do
+ expect_offense(<<~RUBY)
+ class Foo
+ def memoized_method
+ msg = 'This is a memoized method'
+
+ strong_memoize(:memoized_method) do
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `strong_memoize_attr`, instead of using `strong_memoize` directly
+ msg
+ end
+ end
+ end
+ RUBY
+
+ expect_no_corrections
+ end
+ end
+end
diff --git a/spec/rubocop/cop/graphql/descriptions_spec.rb b/spec/rubocop/cop/graphql/descriptions_spec.rb
index 8826e700fdf..0ff2812d6f6 100644
--- a/spec/rubocop/cop/graphql/descriptions_spec.rb
+++ b/spec/rubocop/cop/graphql/descriptions_spec.rb
@@ -16,6 +16,8 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
end
end
TYPE
+
+ expect_no_corrections
end
it 'adds an offense when description does not end in a period' do
@@ -44,6 +46,8 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
end
end
TYPE
+
+ expect_no_corrections
end
it 'adds an offense when description begins with "The"' do
@@ -58,6 +62,46 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
end
end
TYPE
+
+ expect_no_corrections
+ end
+
+ it 'adds an offense when description contains the demonstrative "this"' do
+ expect_offense(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ field :a_thing,
+ ^^^^^^^^^^^^^^^ #{described_class::MSG_CONTAINS_THIS}
+ GraphQL::Types::String,
+ null: false,
+ description: 'Description of this thing.'
+ end
+ end
+ TYPE
+
+ expect_correction(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ field :a_thing,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Description of the thing.'
+ end
+ end
+ TYPE
+ end
+
+ it 'does not add an offense when a word does not contain the substring "this"' do
+ expect_no_offenses(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ field :a_thing,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Description of thistle.'
+ end
+ end
+ TYPE
end
it 'does not add an offense when description is correct' do
@@ -96,6 +140,8 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
end
end
TYPE
+
+ expect_no_corrections
end
it 'adds an offense when description does not end in a period' do
@@ -124,6 +170,8 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
end
end
TYPE
+
+ expect_no_corrections
end
it 'adds an offense when description begins with "The"' do
@@ -138,6 +186,46 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
end
end
TYPE
+
+ expect_no_corrections
+ end
+
+ it 'adds an offense when description contains the demonstrative "this"' do
+ expect_offense(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ argument :a_thing,
+ ^^^^^^^^^^^^^^^^^^ #{described_class::MSG_CONTAINS_THIS}
+ GraphQL::Types::String,
+ null: false,
+ description: 'Description of this thing.'
+ end
+ end
+ TYPE
+
+ expect_correction(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ argument :a_thing,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Description of the thing.'
+ end
+ end
+ TYPE
+ end
+
+ it 'does not add an offense when a word does not contain the substring "this"' do
+ expect_no_offenses(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ argument :a_thing,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Description of thistle.'
+ end
+ end
+ TYPE
end
it 'does not add an offense when description is correct' do
@@ -164,6 +252,8 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
end
end
TYPE
+
+ expect_no_corrections
end
it 'adds an offense when description does not end in a period' do
@@ -186,6 +276,8 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
end
end
TYPE
+
+ expect_no_corrections
end
it 'adds an offense when description begins with "A"' do
@@ -197,6 +289,37 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
end
end
TYPE
+
+ expect_no_corrections
+ end
+
+ it 'adds an offense when description contains the demonstrative "this"' do
+ expect_offense(<<~TYPE.strip)
+ module Types
+ class FakeEnum < BaseEnum
+ value 'FOO', value: 'foo', description: 'Description of this issue.'
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG_CONTAINS_THIS}
+ end
+ end
+ TYPE
+
+ expect_correction(<<~TYPE.strip)
+ module Types
+ class FakeEnum < BaseEnum
+ value 'FOO', value: 'foo', description: 'Description of the issue.'
+ end
+ end
+ TYPE
+ end
+
+ it 'does not add an offense when a word does not contain the substring "this"' do
+ expect_no_offenses(<<~TYPE.strip)
+ module Types
+ class FakeEnum < BaseEnum
+ value 'FOO', value: 'foo', description: 'Description of thistle.'
+ end
+ end
+ TYPE
end
it 'does not add an offense when description is correct (defined using `description:`)' do
@@ -220,8 +343,8 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
end
end
- describe 'autocorrecting descriptions without periods' do
- it 'can autocorrect' do
+ describe 'autocorrecting periods in descriptions' do
+ it 'autocorrects missing periods' do
expect_offense(<<~TYPE)
module Types
class FakeType < BaseObject
@@ -246,7 +369,20 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
TYPE
end
- it 'can autocorrect a heredoc' do
+ it 'does not autocorrect if periods exist' do
+ expect_no_offenses(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ field :a_thing,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Behold! A description.'
+ end
+ end
+ TYPE
+ end
+
+ it 'autocorrects a heredoc' do
expect_offense(<<~TYPE)
module Types
class FakeType < BaseObject
@@ -274,5 +410,104 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
end
TYPE
end
+
+ it 'does not autocorrect a heredoc if periods exist' do
+ expect_no_offenses(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ field :a_thing,
+ GraphQL::Types::String,
+ null: false,
+ description: <<~DESC
+ Behold! A description.
+ DESC
+ end
+ end
+ TYPE
+ end
+ end
+
+ describe 'autocorrecting "this" to "the"' do
+ it 'autocorrects if "this" is found' do
+ expect_offense(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ field :a_thing,
+ ^^^^^^^^^^^^^^^ #{described_class::MSG_CONTAINS_THIS}
+ GraphQL::Types::String,
+ null: false,
+ description: 'Description of this thing.'
+ end
+ end
+ TYPE
+
+ expect_correction(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ field :a_thing,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Description of the thing.'
+ end
+ end
+ TYPE
+ end
+
+ it 'does not autocorrect if "this" is not found' do
+ expect_no_offenses(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ field :a_thing,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Description of the thing.'
+ end
+ end
+ TYPE
+ end
+
+ it 'autocorrects a heredoc if "this" is found' do
+ expect_offense(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ field :a_thing,
+ ^^^^^^^^^^^^^^^ #{described_class::MSG_CONTAINS_THIS}
+ GraphQL::Types::String,
+ null: false,
+ description: <<~DESC
+ Description of this thing.
+ DESC
+ end
+ end
+ TYPE
+
+ expect_correction(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ field :a_thing,
+ GraphQL::Types::String,
+ null: false,
+ description: <<~DESC
+ Description of the thing.
+ DESC
+ end
+ end
+ TYPE
+ end
+
+ it 'does not autocorrect a heredoc if "this" is not found' do
+ expect_no_offenses(<<~TYPE)
+ module Types
+ class FakeType < BaseObject
+ field :a_thing,
+ GraphQL::Types::String,
+ null: false,
+ description: <<~DESC
+ Description of the thing.
+ DESC
+ end
+ end
+ TYPE
+ end
end
end
diff --git a/spec/rubocop/cop/migration/add_column_with_default_spec.rb b/spec/rubocop/cop/migration/add_column_with_default_spec.rb
deleted file mode 100644
index 865f567db44..00000000000
--- a/spec/rubocop/cop/migration/add_column_with_default_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'rubocop_spec_helper'
-require_relative '../../../../rubocop/cop/migration/add_column_with_default'
-
-RSpec.describe RuboCop::Cop::Migration::AddColumnWithDefault do
- let(:cop) { described_class.new }
-
- context 'when outside of a migration' do
- it 'does not register any offenses' do
- expect_no_offenses(<<~RUBY)
- def up
- add_column_with_default(:merge_request_diff_files, :artifacts, :boolean, default: true, allow_null: false)
- end
- RUBY
- end
- end
-
- context 'when in a migration' do
- before do
- allow(cop).to receive(:in_migration?).and_return(true)
- end
-
- it 'registers an offense' do
- expect_offense(<<~RUBY)
- def up
- add_column_with_default(:merge_request_diff_files, :artifacts, :boolean, default: true, allow_null: false)
- ^^^^^^^^^^^^^^^^^^^^^^^ `add_column_with_default` is deprecated, use `add_column` instead
- end
- RUBY
- end
- end
-end
diff --git a/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb b/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb
index 85a86a27c48..a6a072e2caf 100644
--- a/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb
+++ b/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb
@@ -37,8 +37,8 @@ RSpec.describe RuboCop::Cop::Migration::AddLimitToTextColumns do
add_column :test_text_limits, :email, :text
^^^^^^^^^^ #{msg}
- add_column_with_default :test_text_limits, :role, :text, default: 'default'
- ^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
+ add_column :test_text_limits, :role, :text, default: 'default'
+ ^^^^^^^^^^ #{msg}
change_column_type_concurrently :test_text_limits, :test_id, :text
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
@@ -67,7 +67,7 @@ RSpec.describe RuboCop::Cop::Migration::AddLimitToTextColumns do
end
add_column :test_text_limits, :email, :text
- add_column_with_default :test_text_limits, :role, :text, default: 'default'
+ add_column :test_text_limits, :role, :text, default: 'default'
change_column_type_concurrently :test_text_limits, :test_id, :text
add_text_limit :test_text_limits, :name, 255
@@ -115,7 +115,7 @@ RSpec.describe RuboCop::Cop::Migration::AddLimitToTextColumns do
end
add_column :test_text_limits, :email, :text, array: true
- add_column_with_default :test_text_limits, :role, :text, default: [], array: true
+ add_column :test_text_limits, :role, :text, default: [], array: true
change_column_type_concurrently :test_text_limits, :test_id, :text, array: true
end
end
@@ -141,8 +141,8 @@ RSpec.describe RuboCop::Cop::Migration::AddLimitToTextColumns do
add_column :test_text_limits, :email, :text
^^^^^^^^^^ #{msg}
- add_column_with_default :test_text_limits, :role, :text, default: 'default'
- ^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
+ add_column :test_text_limits, :role, :text, default: 'default'
+ ^^^^^^^^^^ #{msg}
change_column_type_concurrently :test_text_limits, :test_id, :text
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
@@ -170,7 +170,7 @@ RSpec.describe RuboCop::Cop::Migration::AddLimitToTextColumns do
end
add_column :encrypted_test_text_limits, :encrypted_email, :text
- add_column_with_default :encrypted_test_text_limits, :encrypted_role, :text, default: 'default'
+ add_column :encrypted_test_text_limits, :encrypted_role, :text, default: 'default'
change_column_type_concurrently :encrypted_test_text_limits, :encrypted_test_id, :text
end
end
@@ -194,7 +194,7 @@ RSpec.describe RuboCop::Cop::Migration::AddLimitToTextColumns do
add_column :no_offense_on_down, :email, :text
- add_column_with_default :no_offense_on_down, :role, :text, default: 'default'
+ add_column :no_offense_on_down, :role, :text, default: 'default'
end
end
RUBY
@@ -215,7 +215,7 @@ RSpec.describe RuboCop::Cop::Migration::AddLimitToTextColumns do
end
add_column :test_text_limits, :email, :text
- add_column_with_default :test_text_limits, :role, :text, default: 'default'
+ add_column :test_text_limits, :role, :text, default: 'default'
change_column_type_concurrently :test_text_limits, :test_id, :text
end
end
diff --git a/spec/rubocop/cop/migration/batch_migrations_post_only_spec.rb b/spec/rubocop/cop/migration/batch_migrations_post_only_spec.rb
new file mode 100644
index 00000000000..b5e2e83e788
--- /dev/null
+++ b/spec/rubocop/cop/migration/batch_migrations_post_only_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/migration/batch_migrations_post_only'
+
+RSpec.describe RuboCop::Cop::Migration::BatchMigrationsPostOnly do
+ let(:cop) { described_class.new }
+
+ before do
+ allow(cop).to receive(:in_post_deployment_migration?).and_return post_migration?
+ end
+
+ context 'when methods appear in a regular migration' do
+ let(:post_migration?) { false }
+
+ it "does not allow 'ensure_batched_background_migration_is_finished' to be called" do
+ expect_offense(<<~CODE)
+ ensure_batched_background_migration_is_finished
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This method must only be used in post-deployment migrations.
+ CODE
+ end
+
+ it "does not allow 'queue_batched_background_migration' to be called" do
+ expect_offense(<<~CODE)
+ queue_batched_background_migration
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This method must only be used in post-deployment migrations.
+ CODE
+ end
+
+ it "does not allow 'delete_batched_background_migration' to be called" do
+ expect_offense(<<~CODE)
+ delete_batched_background_migration
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This method must only be used in post-deployment migrations.
+ CODE
+ end
+
+ it "does not allow 'ensure_batched_background_migration_is_finished' to be called" do
+ expect_offense(<<~CODE)
+ finalize_batched_background_migration
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This method must only be used in post-deployment migrations.
+ CODE
+ end
+
+ it 'allows arbitrary other method to be called' do
+ expect_no_offenses(<<~CODE)
+ foo
+ CODE
+ end
+ end
+
+ context 'when methods appear in a post-deployment migration' do
+ let(:post_migration?) { true }
+
+ it "allows 'ensure_batched_background_migration_is_finished' to be called" do
+ expect_no_offenses(<<~CODE)
+ ensure_batched_background_migration_is_finished
+ CODE
+ end
+
+ it "allows 'queue_batched_background_migration' to be called" do
+ expect_no_offenses(<<~CODE)
+ queue_batched_background_migration
+ CODE
+ end
+
+ it "allows 'delete_batched_background_migration' to be called" do
+ expect_no_offenses(<<~CODE)
+ delete_batched_background_migration
+ CODE
+ end
+
+ it "allows 'ensure_batched_background_migration_is_finished' to be called" do
+ expect_no_offenses(<<~CODE)
+ finalize_batched_background_migration
+ CODE
+ end
+
+ it 'allows arbitrary other method to be called' do
+ expect_no_offenses(<<~CODE)
+ foo
+ CODE
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/prevent_strings_spec.rb b/spec/rubocop/cop/migration/prevent_strings_spec.rb
index f1adeae6786..455b7765aa2 100644
--- a/spec/rubocop/cop/migration/prevent_strings_spec.rb
+++ b/spec/rubocop/cop/migration/prevent_strings_spec.rb
@@ -27,8 +27,8 @@ RSpec.describe RuboCop::Cop::Migration::PreventStrings do
add_column(:users, :bio, :string)
^^^^^^^^^^ %{msg}
- add_column_with_default(:users, :url, :string, default: '/-/user', allow_null: false, limit: 255)
- ^^^^^^^^^^^^^^^^^^^^^^^ %{msg}
+ add_column(:users, :url, :string, default: '/-/user', allow_null: false, limit: 255)
+ ^^^^^^^^^^ %{msg}
change_column_type_concurrently :users, :commit_id, :string
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ %{msg}
@@ -67,7 +67,7 @@ RSpec.describe RuboCop::Cop::Migration::PreventStrings do
end
add_column(:users, :bio, :text)
- add_column_with_default(:users, :url, :text, default: '/-/user', allow_null: false, limit: 255)
+ add_column(:users, :url, :text, default: '/-/user', allow_null: false, limit: 255)
change_column_type_concurrently :users, :commit_id, :text
end
end
@@ -86,7 +86,7 @@ RSpec.describe RuboCop::Cop::Migration::PreventStrings do
end
add_column :test_string_arrays, :email, :string, array: true
- add_column_with_default :test_string_arrays, :role, :string, default: [], array: true
+ add_column :test_string_arrays, :role, :string, default: [], array: true
change_column_type_concurrently :test_string_arrays, :test_id, :string, array: true
end
end
@@ -112,7 +112,7 @@ RSpec.describe RuboCop::Cop::Migration::PreventStrings do
end
add_column(:users, :bio, :string)
- add_column_with_default(:users, :url, :string, default: '/-/user', allow_null: false, limit: 255)
+ add_column(:users, :url, :string, default: '/-/user', allow_null: false, limit: 255)
change_column_type_concurrently :users, :commit_id, :string
end
end
@@ -133,7 +133,7 @@ RSpec.describe RuboCop::Cop::Migration::PreventStrings do
end
add_column(:users, :bio, :string)
- add_column_with_default(:users, :url, :string, default: '/-/user', allow_null: false, limit: 255)
+ add_column(:users, :url, :string, default: '/-/user', allow_null: false, limit: 255)
change_column_type_concurrently :users, :commit_id, :string
end
end
diff --git a/spec/rubocop/cop/migration/versioned_migration_class_spec.rb b/spec/rubocop/cop/migration/versioned_migration_class_spec.rb
index b44f5d64a62..506e3146afa 100644
--- a/spec/rubocop/cop/migration/versioned_migration_class_spec.rb
+++ b/spec/rubocop/cop/migration/versioned_migration_class_spec.rb
@@ -6,7 +6,7 @@ require_relative '../../../../rubocop/cop/migration/versioned_migration_class'
RSpec.describe RuboCop::Cop::Migration::VersionedMigrationClass do
let(:migration) do
<<~SOURCE
- class TestMigration < Gitlab::Database::Migration[1.0]
+ class TestMigration < Gitlab::Database::Migration[2.1]
def up
execute 'select 1'
end
@@ -49,23 +49,23 @@ RSpec.describe RuboCop::Cop::Migration::VersionedMigrationClass do
it 'adds an offence if inheriting from ActiveRecord::Migration' do
expect_offense(<<~RUBY)
class MyMigration < ActiveRecord::Migration[6.1]
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't inherit from ActiveRecord::Migration but use Gitlab::Database::Migration[1.0] instead. See https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning.
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't inherit from ActiveRecord::Migration but use Gitlab::Database::Migration[2.1] instead. See https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning.
end
RUBY
end
it 'adds an offence if including Gitlab::Database::MigrationHelpers directly' do
expect_offense(<<~RUBY)
- class MyMigration < Gitlab::Database::Migration[1.0]
+ class MyMigration < Gitlab::Database::Migration[2.1]
include Gitlab::Database::MigrationHelpers
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't include migration helper modules directly. Inherit from Gitlab::Database::Migration[1.0] instead. See https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning.
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't include migration helper modules directly. Inherit from Gitlab::Database::Migration[2.1] instead. See https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning.
end
RUBY
end
it 'excludes ActiveRecord classes defined inside the migration' do
expect_no_offenses(<<~RUBY)
- class TestMigration < Gitlab::Database::Migration[1.0]
+ class TestMigration < Gitlab::Database::Migration[2.1]
class TestModel < ApplicationRecord
end
diff --git a/spec/rubocop/cop/performance/readlines_each_spec.rb b/spec/rubocop/cop/performance/readlines_each_spec.rb
index d876cbf79a5..11e2cee9262 100644
--- a/spec/rubocop/cop/performance/readlines_each_spec.rb
+++ b/spec/rubocop/cop/performance/readlines_each_spec.rb
@@ -6,7 +6,7 @@ require_relative '../../../../rubocop/cop/performance/readlines_each'
RSpec.describe RuboCop::Cop::Performance::ReadlinesEach do
let(:message) { 'Avoid `IO.readlines.each`, since it reads contents into memory in full. Use `IO.each_line` or `IO.each` instead.' }
- shared_examples_for(:class_read) do |klass|
+ shared_examples_for('class read') do |klass|
context "and it is called as a class method on #{klass}" do
it 'flags it as an offense' do
leading_readline = "#{klass}.readlines(file_path)."
@@ -29,7 +29,7 @@ RSpec.describe RuboCop::Cop::Performance::ReadlinesEach do
context 'when reading all lines using IO.readlines.each' do
%w(IO File).each do |klass|
- it_behaves_like(:class_read, klass)
+ it_behaves_like('class read', klass)
end
context 'and it is called as an instance method on a return value' do
diff --git a/spec/rubocop/cop/rspec/avoid_test_prof_spec.rb b/spec/rubocop/cop/rspec/avoid_test_prof_spec.rb
new file mode 100644
index 00000000000..b180134b226
--- /dev/null
+++ b/spec/rubocop/cop/rspec/avoid_test_prof_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require 'rspec-parameterized'
+
+require_relative '../../../../rubocop/cop/rspec/avoid_test_prof'
+
+RSpec.describe RuboCop::Cop::RSpec::AvoidTestProf, feature_category: :not_owned do
+ using RSpec::Parameterized::TableSyntax
+
+ context 'when there are offenses' do
+ where(:method_call, :method_name, :alternatives) do
+ 'let_it_be(:user)' | 'let_it_be' | '`let` or `let!`'
+ 'let_it_be_with_reload(:user)' | 'let_it_be_with_reload' | '`let` or `let!`'
+ 'let_it_be_with_refind(:user)' | 'let_it_be_with_refind' | '`let` or `let!`'
+ 'before_all' | 'before_all' | '`before` or `before(:all)`'
+ end
+
+ with_them do
+ it 'registers the offense' do
+ error_message = "Prefer #{alternatives} over `#{method_name}` in migration specs. " \
+ 'See ' \
+ 'https://docs.gitlab.com/ee/development/testing_guide/best_practices.html' \
+ '#testprof-in-migration-specs'
+
+ expect_offense(<<~RUBY)
+ describe 'foo' do
+ #{method_call} { table(:users) }
+ #{'^' * method_call.size} #{error_message}
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when there are no offenses' do
+ where(method_call: %w[let(:user) let!(:user) before before(:all)])
+
+ with_them do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ describe 'foo' do
+ #{method_call} { table(:users) }
+ end
+ RUBY
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/rspec/timecop_freeze_spec.rb b/spec/rubocop/cop/rspec/timecop_freeze_spec.rb
deleted file mode 100644
index 4361f587da3..00000000000
--- a/spec/rubocop/cop/rspec/timecop_freeze_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require 'rubocop_spec_helper'
-
-require_relative '../../../../rubocop/cop/rspec/timecop_freeze'
-
-RSpec.describe RuboCop::Cop::RSpec::TimecopFreeze do
- context 'when calling Timecop.freeze' do
- it 'registers an offense and corrects', :aggregate_failures do
- expect_offense(<<~CODE)
- Timecop.freeze(Time.current) { example.run }
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use `Timecop.freeze`, use `freeze_time` instead. [...]
- CODE
-
- expect_correction(<<~CODE)
- freeze_time(Time.current) { example.run }
- CODE
- end
- end
-
- context 'when calling a different method on Timecop' do
- it 'does not register an offense' do
- expect_no_offenses(<<~CODE)
- Timecop.travel(Time.current)
- CODE
- end
- end
-end
diff --git a/spec/rubocop/cop/rspec/timecop_travel_spec.rb b/spec/rubocop/cop/rspec/timecop_travel_spec.rb
deleted file mode 100644
index 89c46ff6c59..00000000000
--- a/spec/rubocop/cop/rspec/timecop_travel_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require 'rubocop_spec_helper'
-
-require_relative '../../../../rubocop/cop/rspec/timecop_travel'
-
-RSpec.describe RuboCop::Cop::RSpec::TimecopTravel do
- context 'when calling Timecop.travel' do
- it 'registers an offense and corrects', :aggregate_failures do
- expect_offense(<<~CODE)
- Timecop.travel(1.day.ago) { create(:issue) }
- ^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use `Timecop.travel`, use `travel_to` instead. [...]
- CODE
-
- expect_correction(<<~CODE)
- travel_to(1.day.ago) { create(:issue) }
- CODE
- end
- end
-
- context 'when calling a different method on Timecop' do
- it 'does not register an offense' do
- expect_no_offenses(<<~CODE)
- Timecop.freeze { create(:issue) }
- CODE
- end
- end
-end
diff --git a/spec/rubocop/cop/user_admin_spec.rb b/spec/rubocop/cop/user_admin_spec.rb
index 99e87d619c0..21bf027324b 100644
--- a/spec/rubocop/cop/user_admin_spec.rb
+++ b/spec/rubocop/cop/user_admin_spec.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
require 'rubocop_spec_helper'
-
-require 'rubocop'
require_relative '../../../rubocop/cop/user_admin'
RSpec.describe RuboCop::Cop::UserAdmin do
diff --git a/spec/rubocop/formatter/graceful_formatter_spec.rb b/spec/rubocop/formatter/graceful_formatter_spec.rb
index 1ed8533ac16..d76e566e2b4 100644
--- a/spec/rubocop/formatter/graceful_formatter_spec.rb
+++ b/spec/rubocop/formatter/graceful_formatter_spec.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'rubocop_spec_helper'
require 'rspec-parameterized'
-require 'rubocop'
-require 'rubocop/rspec/support'
require 'stringio'
require_relative '../../../rubocop/formatter/graceful_formatter'
diff --git a/spec/rubocop/support_workaround.rb b/spec/rubocop/support_workaround.rb
new file mode 100644
index 00000000000..d83aa8a7232
--- /dev/null
+++ b/spec/rubocop/support_workaround.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# This replicates `require 'rubocop/rspec/support'` to workaround the issue
+# in https://gitlab.com/gitlab-org/gitlab/-/issues/382452.
+#
+# All helpers are only included in rubocop specs (type: :rubocop/:rubocop_rspec).
+
+require 'rubocop/rspec/cop_helper'
+require 'rubocop/rspec/host_environment_simulation_helper'
+require 'rubocop/rspec/shared_contexts'
+require 'rubocop/rspec/expect_offense'
+require 'rubocop/rspec/parallel_formatter'
+
+RSpec.configure do |config|
+ config.include CopHelper, type: :rubocop
+ config.include CopHelper, type: :rubocop_rspec
+ config.include HostEnvironmentSimulatorHelper, type: :rubocop
+ config.include HostEnvironmentSimulatorHelper, type: :rubocop_rspec
+ config.include_context 'config', :config
+ config.include_context 'isolated environment', :isolated_environment
+ config.include_context 'maintain registry', :restore_registry
+ config.include_context 'ruby 2.0', :ruby20
+ config.include_context 'ruby 2.1', :ruby21
+ config.include_context 'ruby 2.2', :ruby22
+ config.include_context 'ruby 2.3', :ruby23
+ config.include_context 'ruby 2.4', :ruby24
+ config.include_context 'ruby 2.5', :ruby25
+ config.include_context 'ruby 2.6', :ruby26
+ config.include_context 'ruby 2.7', :ruby27
+ config.include_context 'ruby 3.0', :ruby30
+ config.include_context 'ruby 3.1', :ruby31
+ config.include_context 'ruby 3.2', :ruby32
+end
diff --git a/spec/rubocop_spec_helper.rb b/spec/rubocop_spec_helper.rb
index d57461960f2..9884cdd0272 100644
--- a/spec/rubocop_spec_helper.rb
+++ b/spec/rubocop_spec_helper.rb
@@ -6,9 +6,9 @@ require 'fast_spec_helper'
# To prevent load order issues we need to require `rubocop` first.
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47008
require 'rubocop'
-require 'rubocop/rspec/support'
+require 'rubocop/rspec/shared_contexts/default_rspec_language_config_context'
-require_relative './support/shared_contexts/rubocop_default_rspec_language_config_context'
+require_relative 'rubocop/support_workaround'
RSpec.configure do |config|
config.define_derived_metadata(file_path: %r{spec/rubocop}) do |metadata|
diff --git a/spec/scripts/lib/glfm/shared_spec.rb b/spec/scripts/lib/glfm/shared_spec.rb
index 3ce9d44ba3d..3717c7ce18f 100644
--- a/spec/scripts/lib/glfm/shared_spec.rb
+++ b/spec/scripts/lib/glfm/shared_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Glfm::Shared do
end
describe '#write_file' do
- it 'works' do
+ it 'creates the file' do
filename = Dir::Tmpname.create('basename') do |path|
instance.write_file(path, 'test')
end
@@ -20,7 +20,7 @@ RSpec.describe Glfm::Shared do
end
describe '#run_external_cmd' do
- it 'works' do
+ it 'runs the external command' do
expect(instance.run_external_cmd('echo "hello"')).to eq("hello\n")
end
@@ -35,7 +35,7 @@ RSpec.describe Glfm::Shared do
end
describe '#dump_yaml_with_formatting' do
- it 'works' do
+ it 'returns formatted yaml' do
hash = { a: 'b' }
yaml = instance.dump_yaml_with_formatting(hash, literal_scalars: true)
expect(yaml).to eq("---\na: |-\n b\n")
diff --git a/spec/scripts/lib/glfm/update_specification_spec.rb b/spec/scripts/lib/glfm/update_specification_spec.rb
index ccf1a8fd26a..ed5650e7310 100644
--- a/spec/scripts/lib/glfm/update_specification_spec.rb
+++ b/spec/scripts/lib/glfm/update_specification_spec.rb
@@ -53,9 +53,9 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
let(:ghfm_spec_txt_examples) do
<<~MARKDOWN
- # Section with Examples
+ # Section with examples
- ## Emphasis and Strong
+ ## Emphasis and strong
```````````````````````````````` example
_EMPHASIS LINE 1_
@@ -101,20 +101,24 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
<<~MARKDOWN
# Official Specification Section with Examples
- Some examples.
+ ```````````````````````````````` example
+ official example
+ .
+ <p>official example</p>
+ ````````````````````````````````
+
MARKDOWN
end
let(:glfm_official_specification_md_contents) do
<<~MARKDOWN
- # GLFM Introduction
-
- GLFM intro text.
+ GLFM official text before examples
- <!-- BEGIN TESTS -->
+ #{described_class::BEGIN_TESTS_COMMENT_LINE_TEXT}
#{glfm_official_specification_md_examples}
- <!-- END TESTS -->
- # Non-example official content
+ #{described_class::END_TESTS_COMMENT_LINE_TEXT}
+
+ GLFM official text after examples
MARKDOWN
end
@@ -122,17 +126,19 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
<<~MARKDOWN
# Internal Extension Section with Examples
- Some examples.
+ ```````````````````````````````` example
+ internal example
+ .
+ <p>internal extension example</p>
+ ````````````````````````````````
MARKDOWN
end
let(:glfm_internal_extensions_md_contents) do
<<~MARKDOWN
- # Non-example internal content
- <!-- BEGIN TESTS -->
+ #{described_class::BEGIN_TESTS_COMMENT_LINE_TEXT}
#{glfm_internal_extensions_md_examples}
- <!-- END TESTS -->
- # More non-example internal content
+ #{described_class::END_TESTS_COMMENT_LINE_TEXT}
MARKDOWN
end
@@ -258,29 +264,46 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
describe 'writing output_example_snapshots/snapshot_spec.md' do
let(:es_snapshot_spec_md_contents) { reread_io(es_snapshot_spec_md_io) }
- before do
- subject.process(skip_spec_html_generation: true)
- end
+ context 'with valid glfm_internal_extensions.md' do
+ before do
+ subject.process(skip_spec_html_generation: true)
+ end
- it 'replaces the header text with the GitLab version' do
- expect(es_snapshot_spec_md_contents).not_to match(/GitHub Flavored Markdown Spec/m)
- expect(es_snapshot_spec_md_contents).not_to match(/^version: \d\.\d/m)
- expect(es_snapshot_spec_md_contents).not_to match(/^date: /m)
+ it 'replaces the header text with the GitLab version' do
+ expect(es_snapshot_spec_md_contents).not_to match(/GitHub Flavored Markdown Spec/m)
+ expect(es_snapshot_spec_md_contents).not_to match(/^version: \d\.\d/m)
+ expect(es_snapshot_spec_md_contents).not_to match(/^date: /m)
- expect(es_snapshot_spec_md_contents).to match(/#{Regexp.escape(described_class::GLFM_SPEC_TXT_HEADER)}/mo)
+ expect(es_snapshot_spec_md_contents).to match(/#{Regexp.escape(described_class::ES_SNAPSHOT_SPEC_MD_HEADER)}/mo)
+ end
+
+ it 'includes header and all examples', :unlimited_max_formatted_output_length do
+ # rubocop:disable Style/StringConcatenation (string contatenation is more readable)
+ expected = described_class::ES_SNAPSHOT_SPEC_MD_HEADER +
+ ghfm_spec_txt_examples +
+ "\n" +
+ glfm_official_specification_md_examples +
+ "\n\n" + # NOTE: We want a blank line between the official and internal examples
+ glfm_internal_extensions_md_examples +
+ "\n"
+ # rubocop:enable Style/StringConcatenation
+ expect(es_snapshot_spec_md_contents).to eq(expected)
+ end
end
- it 'includes header and all examples', :unlimited_max_formatted_output_length do
- # rubocop:disable Style/StringConcatenation (string contatenation is more readable)
- expected = described_class::GLFM_SPEC_TXT_HEADER +
- ghfm_spec_txt_examples +
- "\n" +
- glfm_official_specification_md_examples +
- "\n\n" + # NOTE: We want a blank line between the official and internal examples
- glfm_internal_extensions_md_examples +
- "\n"
- # rubocop:enable Style/StringConcatenation
- expect(es_snapshot_spec_md_contents).to eq(expected)
+ context 'with invalid non-example content in glfm_internal_extensions.md' do
+ let(:glfm_internal_extensions_md_contents) do
+ <<~MARKDOWN
+ THIS TEXT IS NOT ALLOWED IN THIS FILE, ONLY EXAMPLES IN BEGIN/END TEST BLOCK ARE ALLOWED
+ #{described_class::BEGIN_TESTS_COMMENT_LINE_TEXT}
+ #{glfm_internal_extensions_md_examples}
+ #{described_class::END_TESTS_COMMENT_LINE_TEXT}
+ MARKDOWN
+ end
+
+ it 'raises an error' do
+ expect { subject.process(skip_spec_html_generation: true) }.to raise_error /no content is allowed outside/i
+ end
end
end
@@ -294,66 +317,56 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
end
it 'renders expected HTML', :unlimited_max_formatted_output_length do
- # NOTE: We do assertions for both output HTML files in this same `it` example block,
+ # NOTE: We do all assertions for both output HTML files in this same `it` example block,
# because calling a full `subject.process` without `skip_spec_html_generation: true`
- # is very slow, and want to avoid doing it twice.
-
- expected_spec_html = <<~RENDERED_HTML
- <div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="1:1-4:3" lang="yaml" class="code highlight js-syntax-highlight language-yaml" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="yaml"><span class="na">title</span><span class="pi">:</span> <span class="s">GitLab Flavored Markdown (GLFM) Spec</span></span>
- <span id="LC2" class="line" lang="yaml"><span class="na">version</span><span class="pi">:</span> <span class="s">alpha</span></span></code></pre>
- <copy-code></copy-code>
- </div>
- <h1 data-sourcepos="5:1-5:19" dir="auto">
- <a id="user-content-glfm-introduction" class="anchor" href="#glfm-introduction" aria-hidden="true"></a>GLFM Introduction</h1>
- <p data-sourcepos="7:1-7:16" dir="auto">GLFM intro text.</p>
-
- <h1 data-sourcepos="10:1-10:46" dir="auto">
- <a id="user-content-official-specification-section-with-examples" class="anchor" href="#official-specification-section-with-examples" aria-hidden="true"></a>Official Specification Section with Examples</h1>
- <p data-sourcepos="12:1-12:14" dir="auto">Some examples.</p>
-
- <h1 data-sourcepos="15:1-15:30" dir="auto">
- <a id="user-content-non-example-official-content" class="anchor" href="#non-example-official-content" aria-hidden="true"></a>Non-example official content</h1>
- RENDERED_HTML
- expect(spec_html_contents).to be == expected_spec_html
-
- expected_snapshot_spec_html = <<~RENDERED_HTML
- <div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="1:1-4:3" lang="yaml" class="code highlight js-syntax-highlight language-yaml" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="yaml"><span class="na">title</span><span class="pi">:</span> <span class="s">GitLab Flavored Markdown (GLFM) Spec</span></span>
- <span id="LC2" class="line" lang="yaml"><span class="na">version</span><span class="pi">:</span> <span class="s">alpha</span></span></code></pre>
- <copy-code></copy-code>
- </div>
- <h1 data-sourcepos="5:1-5:23" dir="auto">
- <a id="user-content-section-with-examples" class="anchor" href="#section-with-examples" aria-hidden="true"></a>Section with Examples</h1>
- <h2 data-sourcepos="7:1-7:22" dir="auto">
- <a id="user-content-emphasis-and-strong" class="anchor" href="#emphasis-and-strong" aria-hidden="true"></a>Emphasis and Strong</h2>
- <div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="9:1-12:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">_EMPHASIS LINE 1_</span>
- <span id="LC2" class="line" lang="plaintext">_EMPHASIS LINE 2_</span></code></pre>
- <copy-code></copy-code>
- </div>
- <div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="14:1-17:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;&lt;em&gt;EMPHASIS LINE 1&lt;/em&gt;</span>
- <span id="LC2" class="line" lang="plaintext">&lt;em&gt;EMPHASIS LINE 2&lt;/em&gt;&lt;/p&gt;</span></code></pre>
- <copy-code></copy-code>
- </div>
- <div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="19:1-21:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">__STRONG!__</span></code></pre>
- <copy-code></copy-code>
- </div>
- <div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="23:1-25:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;&lt;strong&gt;STRONG!&lt;/strong&gt;&lt;/p&gt;</span></code></pre>
- <copy-code></copy-code>
- </div>
- <p data-sourcepos="27:1-27:36" dir="auto">End of last GitHub examples section.</p>
- <h1 data-sourcepos="29:1-29:46" dir="auto">
- <a id="user-content-official-specification-section-with-examples" class="anchor" href="#official-specification-section-with-examples" aria-hidden="true"></a>Official Specification Section with Examples</h1>
- <p data-sourcepos="31:1-31:14" dir="auto">Some examples.</p>
- <h1 data-sourcepos="34:1-34:42" dir="auto">
- <a id="user-content-internal-extension-section-with-examples" class="anchor" href="#internal-extension-section-with-examples" aria-hidden="true"></a>Internal Extension Section with Examples</h1>
- <p data-sourcepos="36:1-36:14" dir="auto">Some examples.</p>
- RENDERED_HTML
- expect(snapshot_spec_html_contents).to be == expected_snapshot_spec_html
+ # is very slow, and want to avoid doing it multiple times
+ #
+ # We also do fairly loose and minimal assertions around the basic structure of the files.
+ # Otherwise, if we asserted the complete exact structure of the HTML, this would be a
+ # brittle test which would breaks every time that something minor changed around the
+ # GLFM rendering. E.g. classes, ids, attribute ordering, etc. All of this behavior
+ # should be thoroughly covered elsewhere by other testing. If there are regressions
+ # in the update specification logic in the future which are not caught by this example,
+ # additional test coverage can be added as necessary.
+
+ # --------------------
+ # spec.html assertions
+ # --------------------
+
+ # correct title should in a header
+ expect(spec_html_contents).to match(%r{<h1.*#{described_class::GLFM_SPEC_TXT_TITLE}</h1>}o)
+
+ # correct text should be included with correct ordering
+ expect(spec_html_contents)
+ .to match(%r{official text before.*official example.*official text after}m)
+
+ # -----------------------------
+ # snapshot_spec.html assertions
+ # -----------------------------
+
+ # correct title should in a header
+ expect(snapshot_spec_html_contents).to match(%r{<h1.*#{described_class::ES_SNAPSHOT_SPEC_TITLE}</h1>}o)
+
+ # correct example text should be included
+ expect(snapshot_spec_html_contents)
+ .to match(%r{internal example}m)
+
+ # -----------------------------
+ # general formatting assertions
+ # -----------------------------
+
+ md = '_EMPHASIS LINE 1_'
+ html = '&lt;em&gt;EMPHASIS LINE 1&lt;/em&gt;'
+
+ # examples should have markdown and HTML in separate pre+code blocks
+ expected_example_1_regex = "<pre.*<code.*#{md}.*</code></pre>.*<pre.*<code.*#{html}.*</code></pre>"
+ expect(snapshot_spec_html_contents).to match(%r{#{expected_example_1_regex}}m)
+
+ # examples should have proper divs prepended for numbering, links, and styling
+ empty_div_for_example_class = '<div>'
+ examplenum_div = '<div><a href="#example-1">Example 1</a></div>'
+ expect(snapshot_spec_html_contents)
+ .to match(%r{#{empty_div_for_example_class}\n#{examplenum_div}.*#{expected_example_1_regex}.*}m)
end
end
# rubocop:enable RSpec/MultipleMemoizedHelpers
diff --git a/spec/scripts/trigger-build_spec.rb b/spec/scripts/trigger-build_spec.rb
index ac8e3c7797c..52682387e20 100644
--- a/spec/scripts/trigger-build_spec.rb
+++ b/spec/scripts/trigger-build_spec.rb
@@ -318,28 +318,6 @@ RSpec.describe Trigger do
end
end
- describe "GITLAB_ASSETS_TAG" do
- context 'when CI_COMMIT_TAG is set' do
- before do
- stub_env('CI_COMMIT_TAG', 'v1.0')
- end
-
- it 'sets GITLAB_ASSETS_TAG to CI_COMMIT_REF_NAME' do
- expect(subject.variables['GITLAB_ASSETS_TAG']).to eq(env['CI_COMMIT_REF_NAME'])
- end
- end
-
- context 'when CI_COMMIT_TAG is nil' do
- before do
- stub_env('CI_COMMIT_TAG', nil)
- end
-
- it 'sets GITLAB_ASSETS_TAG to CI_COMMIT_SHA' do
- expect(subject.variables['GITLAB_ASSETS_TAG']).to eq(env['CI_COMMIT_SHA'])
- end
- end
- end
-
describe "CE_PIPELINE" do
context 'when Trigger.ee? is true' do
before do
diff --git a/spec/serializers/ci/group_variable_entity_spec.rb b/spec/serializers/ci/group_variable_entity_spec.rb
index 9b64e263992..42c4e940421 100644
--- a/spec/serializers/ci/group_variable_entity_spec.rb
+++ b/spec/serializers/ci/group_variable_entity_spec.rb
@@ -3,14 +3,16 @@
require 'spec_helper'
RSpec.describe Ci::GroupVariableEntity do
- let(:variable) { create(:ci_group_variable) }
+ let(:variable) { build(:ci_group_variable) }
let(:entity) { described_class.new(variable) }
describe '#as_json' do
subject { entity.as_json }
it 'contains required fields' do
- expect(subject).to include(:id, :key, :value, :protected, :variable_type, :environment_scope)
+ expect(subject.keys).to contain_exactly(
+ :id, :key, :value, :protected, :variable_type, :environment_scope, :raw, :masked
+ )
end
end
end
diff --git a/spec/serializers/ci/variable_entity_spec.rb b/spec/serializers/ci/variable_entity_spec.rb
index 38da0b16bbd..96111604028 100644
--- a/spec/serializers/ci/variable_entity_spec.rb
+++ b/spec/serializers/ci/variable_entity_spec.rb
@@ -3,14 +3,16 @@
require 'spec_helper'
RSpec.describe Ci::VariableEntity do
- let(:variable) { create(:ci_variable) }
+ let(:variable) { build(:ci_variable) }
let(:entity) { described_class.new(variable) }
describe '#as_json' do
subject { entity.as_json }
it 'contains required fields' do
- expect(subject).to include(:id, :key, :value, :protected, :environment_scope, :variable_type)
+ expect(subject.keys).to contain_exactly(
+ :id, :key, :value, :protected, :environment_scope, :variable_type, :raw, :masked
+ )
end
end
end
diff --git a/spec/serializers/deploy_keys/deploy_key_entity_spec.rb b/spec/serializers/deploy_keys/deploy_key_entity_spec.rb
index 7719cafae11..4302ed3a097 100644
--- a/spec/serializers/deploy_keys/deploy_key_entity_spec.rb
+++ b/spec/serializers/deploy_keys/deploy_key_entity_spec.rb
@@ -39,7 +39,8 @@ RSpec.describe DeployKeys::DeployKeyEntity do
id: project.id,
name: project.name,
full_path: project_path(project),
- full_name: project.full_name
+ full_name: project.full_name,
+ refs_url: refs_project_path(project)
}
}
]
diff --git a/spec/serializers/entity_date_helper_spec.rb b/spec/serializers/entity_date_helper_spec.rb
index a8c338675e2..5a4571339b3 100644
--- a/spec/serializers/entity_date_helper_spec.rb
+++ b/spec/serializers/entity_date_helper_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe EntityDateHelper do
describe '#remaining_days_in_words' do
around do |example|
- Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
+ travel_to(Time.utc(2017, 3, 17)) { example.run }
end
context 'when less than 31 days remaining' do
@@ -75,7 +75,9 @@ RSpec.describe EntityDateHelper do
end
it 'returns 1 day remaining when queried mid-day' do
- Timecop.freeze(Time.utc(2017, 3, 17, 13, 10)) do
+ travel_back
+
+ travel_to(Time.utc(2017, 3, 17, 13, 10)) do
expect(milestone_remaining).to eq("<strong>1</strong> day remaining")
end
end
diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb
index 6161d4d7ec2..9d53d8bb235 100644
--- a/spec/serializers/issue_entity_spec.rb
+++ b/spec/serializers/issue_entity_spec.rb
@@ -162,4 +162,24 @@ RSpec.describe IssueEntity do
end
it_behaves_like 'issuable entity current_user properties'
+
+ context 'when issue has email participants' do
+ before do
+ resource.issue_email_participants.create!(email: 'any@email.com')
+ end
+
+ context 'when issue is confidential' do
+ it 'returns email participants' do
+ resource.update!(confidential: true)
+
+ expect(subject[:issue_email_participants]).to match_array([{ email: "any@email.com" }])
+ end
+ end
+
+ context 'when issue is not confidential' do
+ it 'returns empty array' do
+ expect(subject[:issue_email_participants]).to be_empty
+ end
+ end
+ end
end
diff --git a/spec/serializers/linked_project_issue_entity_spec.rb b/spec/serializers/linked_project_issue_entity_spec.rb
index 523b89921b6..d415d1cbcb2 100644
--- a/spec/serializers/linked_project_issue_entity_spec.rb
+++ b/spec/serializers/linked_project_issue_entity_spec.rb
@@ -6,10 +6,10 @@ RSpec.describe LinkedProjectIssueEntity do
include Gitlab::Routing.url_helpers
let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
let_it_be(:issue_link) { create(:issue_link) }
let(:request) { double('request') }
+ let(:issue_type) { :task }
let(:related_issue) { issue_link.source.related_issues(user).first }
let(:entity) { described_class.new(related_issue, request: request, current_user: user) }
@@ -31,9 +31,7 @@ RSpec.describe LinkedProjectIssueEntity do
end
context 'when related issue is a task' do
- before do
- related_issue.update!(issue_type: :task, work_item_type: WorkItems::Type.default_by_type(:task))
- end
+ let_it_be(:issue_link) { create(:issue_link, target: create(:issue, :task)) }
it 'returns a work item issue type' do
expect(serialized_entity).to include(type: 'TASK')
@@ -47,9 +45,7 @@ RSpec.describe LinkedProjectIssueEntity do
end
context 'when related issue is a task' do
- before do
- related_issue.update!(issue_type: :task, work_item_type: WorkItems::Type.default_by_type(:task))
- end
+ let_it_be(:issue_link) { create(:issue_link, target: create(:issue, :task)) }
context 'when use_iid_in_work_items_path feature flag is disabled' do
before do
diff --git a/spec/serializers/member_entity_spec.rb b/spec/serializers/member_entity_spec.rb
index 370fa14b1e8..350355eb72d 100644
--- a/spec/serializers/member_entity_spec.rb
+++ b/spec/serializers/member_entity_spec.rb
@@ -90,6 +90,28 @@ RSpec.describe MemberEntity do
it_behaves_like 'is_direct_member'
end
+ context 'is_last_owner' do
+ context 'when member is last owner' do
+ before do
+ allow(member).to receive(:last_owner?).and_return(true)
+ end
+
+ it 'exposes `is_last_owner` as `true`' do
+ expect(entity_hash[:is_last_owner]).to be(true)
+ end
+ end
+
+ context 'when owner is not last owner' do
+ before do
+ allow(member).to receive(:last_owner?).and_return(false)
+ end
+
+ it 'exposes `is_last_owner` as `false`' do
+ expect(entity_hash[:is_last_owner]).to be(false)
+ end
+ end
+ end
+
context 'new member user state is blocked_pending_approval' do
let(:user) { create(:user, :blocked_pending_approval) }
let(:group_member) { create(:group_member, :invited, group: group, invite_email: user.email) }
diff --git a/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb b/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
index 702c6d9fe98..f883156628a 100644
--- a/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
@@ -152,10 +152,10 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
.to eq(closed_event.author_id)
expect(subject.dig(:metrics, :merged_at).to_s)
- .to eq(merge_event.updated_at.to_s)
+ .to eq(merge_event.updated_at.iso8601)
expect(subject.dig(:metrics, :closed_at).to_s)
- .to eq(closed_event.updated_at.to_s)
+ .to eq(closed_event.updated_at.iso8601)
end
end
diff --git a/spec/serializers/merge_request_user_entity_spec.rb b/spec/serializers/merge_request_user_entity_spec.rb
index 5c7120ab6a4..3e136f29247 100644
--- a/spec/serializers/merge_request_user_entity_spec.rb
+++ b/spec/serializers/merge_request_user_entity_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe MergeRequestUserEntity do
- let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:user) { create(:user) }
let_it_be(:merge_request) { create(:merge_request, assignees: [user]) }
let(:request) { EntityRequest.new(project: merge_request.target_project, current_user: user) }
diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb
index ca38721cc7f..764d55e8b4a 100644
--- a/spec/serializers/pipeline_details_entity_spec.rb
+++ b/spec/serializers/pipeline_details_entity_spec.rb
@@ -11,16 +11,16 @@ RSpec.describe PipelineDetailsEntity do
described_class.represent(pipeline, request: request)
end
- it 'inherits from PipelineEntity' do
- expect(described_class).to be < Ci::PipelineEntity
- end
-
before do
stub_not_protect_default_branch
allow(request).to receive(:current_user).and_return(user)
end
+ it 'inherits from PipelineEntity' do
+ expect(described_class).to be < Ci::PipelineEntity
+ end
+
describe '#as_json' do
subject { entity.as_json }
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 9caaeb3450b..33fee68a2f2 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -183,25 +183,25 @@ RSpec.describe PipelineSerializer do
context 'with triggered pipelines' do
before do
- pipeline_1 = create(:ci_pipeline)
+ pipeline_1 = create(:ci_pipeline, project: project)
build_1 = create(:ci_build, pipeline: pipeline_1)
create(:ci_sources_pipeline, source_job: build_1)
-
- pipeline_2 = create(:ci_pipeline)
- build_2 = create(:ci_build, pipeline: pipeline_2)
- create(:ci_sources_pipeline, source_job: build_2)
end
it 'verifies number of queries', :request_store do
- recorded = ActiveRecord::QueryRecorder.new { subject }
+ control = ActiveRecord::QueryRecorder.new do
+ serializer.represent(Ci::Pipeline.all, preload: true)
+ end
- # Existing numbers are high and require performance optimization
- # Ongoing issue:
- # https://gitlab.com/gitlab-org/gitlab/-/issues/225156
- expected_queries = Gitlab.ee? ? 78 : 74
+ pipeline_2 = create(:ci_pipeline, project: project)
+ build_2 = create(:ci_build, pipeline: pipeline_2)
+ create(:ci_sources_pipeline, source_job: build_2)
- expect(recorded.count).to be_within(2).of(expected_queries)
- expect(recorded.cached_count).to eq(0)
+ recorded = ActiveRecord::QueryRecorder.new do
+ serializer.represent(Ci::Pipeline.all, preload: true)
+ end
+
+ expect(recorded).not_to exceed_query_limit(control)
end
end
diff --git a/spec/serializers/prometheus_alert_entity_spec.rb b/spec/serializers/prometheus_alert_entity_spec.rb
index 91a1e3377c2..02da5a5bb88 100644
--- a/spec/serializers/prometheus_alert_entity_spec.rb
+++ b/spec/serializers/prometheus_alert_entity_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe PrometheusAlertEntity do
- let(:user) { create(:user) }
- let(:prometheus_alert) { create(:prometheus_alert) }
+ let(:user) { build_stubbed(:user) }
+ let(:prometheus_alert) { build_stubbed(:prometheus_alert) }
let(:request) { double('prometheus_alert', current_user: user) }
let(:entity) { described_class.new(prometheus_alert, request: request) }
diff --git a/spec/serializers/release_serializer_spec.rb b/spec/serializers/release_serializer_spec.rb
index b31172c3a50..e9f56e3453e 100644
--- a/spec/serializers/release_serializer_spec.rb
+++ b/spec/serializers/release_serializer_spec.rb
@@ -3,18 +3,13 @@
require 'spec_helper'
RSpec.describe ReleaseSerializer do
- let(:user) { create(:user) }
- let(:project) { create :project }
+ let(:user) { build_stubbed(:user) }
subject { described_class.new.represent(resource, current_user: user) }
- before do
- project.add_developer(user)
- end
-
describe '#represent' do
context 'when a single object is being serialized' do
- let(:resource) { create(:release, project: project) }
+ let(:resource) { build_stubbed(:release) }
it 'serializes the label object' do
expect(subject[:tag]).to eq resource.tag
@@ -26,7 +21,7 @@ RSpec.describe ReleaseSerializer do
end
context 'when multiple objects are being serialized' do
- let(:resource) { create_list(:release, 3) }
+ let(:resource) { build_stubbed_list(:release, 3) }
it 'serializes the array of releases' do
expect(subject.size).to eq(3)
diff --git a/spec/services/admin/set_feature_flag_service_spec.rb b/spec/services/admin/set_feature_flag_service_spec.rb
index 9a9c5545e23..45ee914558a 100644
--- a/spec/services/admin/set_feature_flag_service_spec.rb
+++ b/spec/services/admin/set_feature_flag_service_spec.rb
@@ -13,11 +13,69 @@ RSpec.describe Admin::SetFeatureFlagService do
# Find any `development` feature flag name
let(:known_feature_flag) do
Feature::Definition.definitions
- .values.find(&:development?)
+ .values.find { |defn| defn.development? && !defn.default_enabled }
+ end
+
+ describe 'sequences of executions' do
+ subject(:flag) do
+ Feature.get(feature_name) # rubocop: disable Gitlab/AvoidFeatureGet
+ end
+
+ context 'if we enable_percentage_of_actors and then disable' do
+ before do
+ described_class
+ .new(feature_flag_name: feature_name, params: { key: 'percentage_of_actors', value: '50.0' })
+ .execute
+
+ described_class
+ .new(feature_flag_name: feature_name, params: { key: 'percentage_of_actors', value: '0.0' })
+ .execute
+ end
+
+ it 'leaves the flag off' do
+ expect(flag.state).to eq(:off)
+ end
+ end
+
+ context 'if we enable and then enable_percentage_of_actors' do
+ before do
+ described_class
+ .new(feature_flag_name: feature_name, params: { key: 'percentage_of_actors', value: '100.0' })
+ .execute
+ end
+
+ it 'reports an error' do
+ result = described_class
+ .new(feature_flag_name: feature_name, params: { key: 'percentage_of_actors', value: '50.0' })
+ .execute
+
+ expect(flag.state).to eq(:on)
+ expect(result).to be_error
+ end
+
+ context 'if we disable the flag first' do
+ before do
+ described_class
+ .new(feature_flag_name: feature_name, params: { value: 'false' })
+ .execute
+ end
+
+ it 'sets the percentage of actors' do
+ result = described_class
+ .new(feature_flag_name: feature_name, params: { key: 'percentage_of_actors', value: '50.0' })
+ .execute
+
+ expect(flag.state).to eq(:conditional)
+ expect(result).not_to be_error
+ end
+ end
+ end
end
describe '#execute' do
before do
+ unstub_all_feature_flags
+
Feature.reset
Flipper.unregister_groups
Flipper.register(:perf_team) do |actor|
@@ -25,7 +83,87 @@ RSpec.describe Admin::SetFeatureFlagService do
end
end
- subject { service.execute }
+ subject(:result) { service.execute }
+
+ context 'when we cannot interpret the operation' do
+ let(:params) { { value: 'wibble', key: 'unknown' } }
+
+ it { is_expected.to be_error }
+ it { is_expected.to have_attributes(reason: :illegal_operation) }
+ it { is_expected.to have_attributes(message: %(Cannot set '#{feature_name}' ("unknown") to "wibble")) }
+
+ context 'when the key is absent' do
+ let(:params) { { value: 'wibble' } }
+
+ it { is_expected.to be_error }
+ it { is_expected.to have_attributes(reason: :illegal_operation) }
+ it { is_expected.to have_attributes(message: %(Cannot set '#{feature_name}' to "wibble")) }
+ end
+ end
+
+ context 'when the value to set cannot be parsed' do
+ let(:params) { { value: 'wibble', key: 'percentage_of_actors' } }
+
+ it { is_expected.to be_error }
+ it { is_expected.to have_attributes(reason: :illegal_operation) }
+ it { is_expected.to have_attributes(message: 'Not a percentage') }
+ end
+
+ context 'when value is "remove_opt_out"' do
+ before do
+ Feature.enable(feature_name)
+ end
+
+ context 'without a target' do
+ let(:params) { { value: 'remove_opt_out' } }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:illegal_operation)
+ end
+ end
+
+ context 'with a target' do
+ let(:params) { { value: 'remove_opt_out', user: user.username } }
+
+ context 'when there is currently no opt-out' do
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:illegal_operation)
+ expect(Feature).to be_enabled(feature_name, user)
+ end
+ end
+
+ context 'when there is currently an opt-out' do
+ before do
+ Feature.opt_out(feature_name, user)
+ end
+
+ it 'removes the opt out' do
+ expect(result).to be_success
+ expect(Feature).to be_enabled(feature_name, user)
+ end
+ end
+ end
+ end
+
+ context 'when value is "opt_out"' do
+ let(:params) { { value: 'opt_out', namespace: group.full_path, user: user.username } }
+
+ it 'opts the user and group out' do
+ expect(Feature).to receive(:opt_out).with(feature_name, user)
+ expect(Feature).to receive(:opt_out).with(feature_name, group)
+ expect(result).to be_success
+ end
+
+ context 'without a target' do
+ let(:params) { { value: 'opt_out' } }
+
+ it { is_expected.to be_error }
+
+ it { is_expected.to have_attributes(reason: :illegal_operation) }
+ end
+ end
context 'when enabling the feature flag' do
let(:params) { { value: 'true' } }
@@ -38,6 +176,18 @@ RSpec.describe Admin::SetFeatureFlagService do
expect(feature_flag.name).to eq(feature_name)
end
+ context 'when the flag is default_enabled' do
+ let(:known_feature_flag) do
+ Feature::Definition.definitions
+ .values.find { |defn| defn.development? && defn.default_enabled }
+ end
+
+ it 'leaves the flag enabled' do
+ expect(subject).to be_success
+ expect(Feature).to be_enabled(feature_name)
+ end
+ end
+
it 'logs the event' do
expect(Feature.logger).to receive(:info).once
@@ -52,6 +202,31 @@ RSpec.describe Admin::SetFeatureFlagService do
expect(subject).to be_success
end
+ context 'when the flag has been opted out for user' do
+ before do
+ Feature.enable(feature_name)
+ Feature.opt_out(feature_name, user)
+ end
+
+ it 'records an error' do
+ expect(subject).to be_error
+ expect(subject.reason).to eq(:illegal_operation)
+ expect(Feature).not_to be_enabled(feature_name, user)
+ end
+ end
+
+ context 'when the flag is default_enabled' do
+ let(:known_feature_flag) do
+ Feature::Definition.definitions
+ .values.find { |defn| defn.development? && defn.default_enabled }
+ end
+
+ it 'leaves the feature enabled' do
+ expect(subject).to be_success
+ expect(Feature).to be_enabled(feature_name, user)
+ end
+ end
+
context 'when user does not exist' do
let(:params) { { value: 'true', user: 'unknown-user' } }
@@ -166,6 +341,16 @@ RSpec.describe Admin::SetFeatureFlagService do
expect(subject).to be_success
end
end
+
+ context 'with a target' do
+ before do
+ params[:user] = user.username
+ end
+
+ it { is_expected.to be_error }
+
+ it { is_expected.to have_attributes(reason: :illegal_operation) }
+ end
end
context 'when enabling given a percentage of actors' do
@@ -184,6 +369,16 @@ RSpec.describe Admin::SetFeatureFlagService do
expect(subject).to be_success
end
end
+
+ context 'with a target' do
+ before do
+ params[:user] = user.username
+ end
+
+ it { is_expected.to be_error }
+
+ it { is_expected.to have_attributes(reason: :illegal_operation) }
+ end
end
end
diff --git a/spec/services/bulk_imports/create_service_spec.rb b/spec/services/bulk_imports/create_service_spec.rb
index bf174f5d5a2..f1e5533139e 100644
--- a/spec/services/bulk_imports/create_service_spec.rb
+++ b/spec/services/bulk_imports/create_service_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe BulkImports::CreateService do
let(:user) { create(:user) }
let(:credentials) { { url: 'http://gitlab.example', access_token: 'token' } }
+ let(:destination_group) { create(:group, path: 'destination1') }
+ let_it_be(:parent_group) { create(:group, path: 'parent-group') }
let(:params) do
[
{
@@ -39,10 +41,12 @@ RSpec.describe BulkImports::CreateService do
before do
allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
allow(instance).to receive(:instance_version).and_return(source_version)
+ allow(instance).to receive(:instance_enterprise).and_return(false)
end
end
it 'creates bulk import' do
+ parent_group.add_owner(user)
expect { subject.execute }.to change { BulkImport.count }.by(1)
last_bulk_import = BulkImport.last
@@ -50,11 +54,21 @@ RSpec.describe BulkImports::CreateService do
expect(last_bulk_import.user).to eq(user)
expect(last_bulk_import.source_version).to eq(source_version.to_s)
expect(last_bulk_import.user).to eq(user)
+ expect(last_bulk_import.source_enterprise).to eq(false)
+
expect_snowplow_event(
category: 'BulkImports::CreateService',
action: 'create',
label: 'bulk_import_group'
)
+
+ expect_snowplow_event(
+ category: 'BulkImports::CreateService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Owner', import_type: 'bulk_import_group' }
+ )
end
it 'creates bulk import entities' do
@@ -87,5 +101,109 @@ RSpec.describe BulkImports::CreateService do
expect(result).to be_error
expect(result.message).to eq("Validation failed: Source full path can't be blank")
end
+
+ describe '#user-role' do
+ context 'when there is a parent_namespace and the user is a member' do
+ let(:group2) { create(:group, path: 'destination200', source_id: parent_group.id ) }
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full/path/to/group1',
+ destination_slug: 'destination200',
+ destination_namespace: 'parent-group'
+ }
+ ]
+ end
+
+ it 'defines access_level from parent namespace membership' do
+ parent_group.add_guest(user)
+ subject.execute
+
+ expect_snowplow_event(
+ category: 'BulkImports::CreateService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Guest', import_type: 'bulk_import_group' }
+ )
+ end
+ end
+
+ context 'when there is a parent_namespace and the user is not a member' do
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full/path/to/group1',
+ destination_slug: 'destination-group-1',
+ destination_namespace: 'parent-group'
+ }
+ ]
+ end
+
+ it 'defines access_level as not a member' do
+ subject.execute
+ expect_snowplow_event(
+ category: 'BulkImports::CreateService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Not a member', import_type: 'bulk_import_group' }
+ )
+ end
+ end
+
+ context 'when there is a destination_namespace but no parent_namespace' do
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full/path/to/group1',
+ destination_slug: 'destination-group-1',
+ destination_namespace: 'destination1'
+ }
+ ]
+ end
+
+ it 'defines access_level from destination_namespace' do
+ destination_group.add_developer(user)
+ subject.execute
+
+ expect_snowplow_event(
+ category: 'BulkImports::CreateService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Developer', import_type: 'bulk_import_group' }
+ )
+ end
+ end
+
+ context 'when there is no destination_namespace or parent_namespace' do
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full/path/to/group1',
+ destination_slug: 'destinationational mcdestiny',
+ destination_namespace: 'destinational-mcdestiny'
+ }
+ ]
+ end
+
+ it 'defines access_level as owner' do
+ subject.execute
+
+ expect_snowplow_event(
+ category: 'BulkImports::CreateService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Owner', import_type: 'bulk_import_group' }
+ )
+ end
+ end
+ end
end
end
diff --git a/spec/services/bulk_imports/file_download_service_spec.rb b/spec/services/bulk_imports/file_download_service_spec.rb
index ec9cc719e24..27f77b678e3 100644
--- a/spec/services/bulk_imports/file_download_service_spec.rb
+++ b/spec/services/bulk_imports/file_download_service_spec.rb
@@ -14,22 +14,19 @@ RSpec.describe BulkImports::FileDownloadService do
let_it_be(:filepath) { File.join(tmpdir, filename) }
let_it_be(:content_length) { 1000 }
- let(:chunk_double) { double('chunk', size: 100, code: 200) }
-
- let(:response_double) do
- double(
- code: 200,
- success?: true,
- parsed_response: {},
- headers: {
- 'content-length' => content_length,
- 'content-type' => content_type,
- 'content-disposition' => content_disposition
- }
- )
+ let(:headers) do
+ {
+ 'content-length' => content_length,
+ 'content-type' => content_type,
+ 'content-disposition' => content_disposition
+ }
end
- subject do
+ let(:chunk_size) { 100 }
+ let(:chunk_code) { 200 }
+ let(:chunk_double) { double('chunk', size: chunk_size, code: chunk_code, http_response: double(to_hash: headers)) }
+
+ subject(:service) do
described_class.new(
configuration: config,
relative_url: '/test',
@@ -42,9 +39,10 @@ RSpec.describe BulkImports::FileDownloadService do
before do
allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
- allow(client).to receive(:head).and_return(response_double)
allow(client).to receive(:stream).and_yield(chunk_double)
end
+
+ allow(service).to receive(:response_headers).and_return(headers)
end
shared_examples 'downloads file' do
@@ -136,7 +134,7 @@ RSpec.describe BulkImports::FileDownloadService do
end
context 'when chunk code is not 200' do
- let(:chunk_double) { double('chunk', size: 1000, code: 500) }
+ let(:chunk_code) { 500 }
it 'raises an error' do
expect { subject.execute }.to raise_error(
@@ -146,7 +144,7 @@ RSpec.describe BulkImports::FileDownloadService do
end
context 'when chunk code is redirection' do
- let(:chunk_double) { double('redirection', size: 1000, code: 303) }
+ let(:chunk_code) { 303 }
it 'does not write a redirection chunk' do
expect { subject.execute }.not_to raise_error
@@ -157,10 +155,9 @@ RSpec.describe BulkImports::FileDownloadService do
context 'when redirection chunk appears at a later stage of the download' do
it 'raises an error' do
another_chunk_double = double('another redirection', size: 1000, code: 303)
- data_chunk = double('data chunk', size: 1000, code: 200)
+ data_chunk = double('data chunk', size: 1000, code: 200, http_response: double(to_hash: {}))
allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
- allow(client).to receive(:head).and_return(response_double)
allow(client)
.to receive(:stream)
.and_yield(chunk_double)
@@ -177,6 +174,51 @@ RSpec.describe BulkImports::FileDownloadService do
end
end
+ describe 'remote content validation' do
+ context 'on redirect chunk' do
+ let(:chunk_code) { 303 }
+
+ it 'does not run content type & length validations' do
+ expect(service).not_to receive(:validate_content_type)
+ expect(service).not_to receive(:validate_content_length)
+
+ service.execute
+ end
+ end
+
+ context 'when there is one data chunk' do
+ it 'validates content type & length' do
+ expect(service).to receive(:validate_content_type)
+ expect(service).to receive(:validate_content_length)
+
+ service.execute
+ end
+ end
+
+ context 'when there are multiple data chunks' do
+ it 'validates content type & length only once' do
+ data_chunk = double(
+ 'data chunk',
+ size: 1000,
+ code: 200,
+ http_response: double(to_hash: {})
+ )
+
+ allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
+ allow(client)
+ .to receive(:stream)
+ .and_yield(chunk_double)
+ .and_yield(data_chunk)
+ end
+
+ expect(service).to receive(:validate_content_type).once
+ expect(service).to receive(:validate_content_length).once
+
+ service.execute
+ end
+ end
+ end
+
context 'when file is a symlink' do
let_it_be(:symlink) { File.join(tmpdir, 'symlink') }
diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb
index 4b0a1204558..10cb0a2f065 100644
--- a/spec/services/chat_names/find_user_service_spec.rb
+++ b/spec/services/chat_names/find_user_service_spec.rb
@@ -4,17 +4,16 @@ require 'spec_helper'
RSpec.describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state do
describe '#execute' do
- let(:integration) { create(:integration) }
-
- subject { described_class.new(integration, params).execute }
+ subject { described_class.new(team_id, user_id).execute }
context 'find user mapping' do
- let(:user) { create(:user) }
- let!(:chat_name) { create(:chat_name, user: user, integration: integration) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:chat_name) { create(:chat_name, user: user) }
- context 'when existing user is requested' do
- let(:params) { { team_id: chat_name.team_id, user_id: chat_name.chat_id } }
+ let(:team_id) { chat_name.team_id }
+ let(:user_id) { chat_name.chat_id }
+ context 'when existing user is requested' do
it 'returns the existing chat_name' do
is_expected.to eq(chat_name)
end
@@ -28,7 +27,8 @@ RSpec.describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state do
end
it 'only updates an existing timestamp once within a certain time frame' do
- service = described_class.new(integration, params)
+ chat_name = create(:chat_name, user: user)
+ service = described_class.new(team_id, user_id)
expect(chat_name.last_used_at).to be_nil
@@ -43,9 +43,9 @@ RSpec.describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state do
end
context 'when different user is requested' do
- let(:params) { { team_id: chat_name.team_id, user_id: 'non-existing-user' } }
+ let(:user_id) { 'non-existing-user' }
- it 'returns existing user' do
+ it 'returns nil' do
is_expected.to be_nil
end
end
diff --git a/spec/services/ci/append_build_trace_service_spec.rb b/spec/services/ci/append_build_trace_service_spec.rb
index 487dbacbe90..20f7967d1f1 100644
--- a/spec/services/ci/append_build_trace_service_spec.rb
+++ b/spec/services/ci/append_build_trace_service_spec.rb
@@ -76,4 +76,36 @@ RSpec.describe Ci::AppendBuildTraceService do
expect(build.failure_reason).to eq 'trace_size_exceeded'
end
end
+
+ context 'when debug_trace param is provided' do
+ let(:metadata) { Ci::BuildMetadata.find_by(build_id: build) }
+ let(:stream_size) { 192.kilobytes }
+ let(:body_data) { 'x' * stream_size }
+ let(:content_range) { "#{body_start}-#{stream_size}" }
+
+ context 'when sending the first trace' do
+ let(:body_start) { 0 }
+
+ it 'updates build metadata debug_trace_enabled' do
+ described_class
+ .new(build, content_range: content_range, debug_trace: true)
+ .execute(body_data)
+
+ expect(metadata.debug_trace_enabled).to be(true)
+ end
+ end
+
+ context 'when sending the second trace' do
+ let(:body_start) { 1 }
+
+ it 'does not update build metadata debug_trace_enabled', :aggregate_failures do
+ query_recorder = ActiveRecord::QueryRecorder.new do
+ described_class.new(build, content_range: content_range, debug_trace: true).execute(body_data)
+ end
+
+ expect(metadata.debug_trace_enabled).to be(false)
+ expect(query_recorder.log).not_to include(/p_ci_builds_metadata/)
+ end
+ end
+ end
end
diff --git a/spec/services/ci/create_downstream_pipeline_service_spec.rb b/spec/services/ci/create_downstream_pipeline_service_spec.rb
index 9c02c5218f1..bcdb2b4f796 100644
--- a/spec/services/ci/create_downstream_pipeline_service_spec.rb
+++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
+RSpec.describe Ci::CreateDownstreamPipelineService, '#execute', feature_category: :continuous_integration do
include Ci::SourcePipelineHelpers
# Using let_it_be on user and projects for these specs can cause
@@ -13,7 +13,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
let(:downstream_project) { create(:project, :repository) }
let!(:upstream_pipeline) do
- create(:ci_pipeline, :running, project: upstream_project)
+ create(:ci_pipeline, :created, project: upstream_project)
end
let(:trigger) do
@@ -33,6 +33,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
let(:service) { described_class.new(upstream_project, user) }
+ let(:pipeline) { subject.payload }
before do
upstream_project.add_developer(user)
@@ -40,6 +41,12 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
subject { service.execute(bridge) }
+ shared_context 'when ci_bridge_remove_sourced_pipelines is disabled' do
+ before do
+ stub_feature_flags(ci_bridge_remove_sourced_pipelines: false)
+ end
+ end
+
context 'when downstream project has not been found' do
let(:trigger) do
{ trigger: { project: 'unknown/project' } }
@@ -48,6 +55,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a pipeline' do
expect { subject }
.not_to change { Ci::Pipeline.count }
+ expect(subject).to be_error
+ expect(subject.message).to eq("Pre-conditions not met")
end
it 'changes pipeline bridge job status to failed' do
@@ -63,9 +72,11 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a new pipeline' do
expect { subject }
.not_to change { Ci::Pipeline.count }
+ expect(subject).to be_error
+ expect(subject.message).to eq("Pre-conditions not met")
end
- it 'changes status of the bridge build' do
+ it 'changes status of the bridge build to failed' do
subject
expect(bridge.reload).to be_failed
@@ -82,9 +93,11 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a new pipeline' do
expect { subject }
.not_to change { Ci::Pipeline.count }
+ expect(subject).to be_error
+ expect(subject.message).to eq("Pre-conditions not met")
end
- it 'changes status of the bridge build' do
+ it 'changes status of the bridge build to failed' do
subject
expect(bridge.reload).to be_failed
@@ -103,35 +116,51 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates only one new pipeline' do
expect { subject }
.to change { Ci::Pipeline.count }.by(1)
+ expect(subject).to be_success
end
it 'creates a new pipeline in a downstream project' do
- pipeline = subject
-
expect(pipeline.user).to eq bridge.user
expect(pipeline.project).to eq downstream_project
- expect(bridge.sourced_pipelines.first.pipeline).to eq pipeline
+ expect(bridge.reload.sourced_pipeline.pipeline).to eq pipeline
expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
expect(pipeline.source_bridge).to eq bridge
expect(pipeline.source_bridge).to be_a ::Ci::Bridge
end
+ context 'when ci_bridge_remove_sourced_pipelines is disabled' do
+ include_context 'when ci_bridge_remove_sourced_pipelines is disabled'
+
+ it 'creates a new pipeline in a downstream project' do
+ expect(pipeline.user).to eq bridge.user
+ expect(pipeline.project).to eq downstream_project
+ expect(bridge.sourced_pipelines.first.pipeline).to eq pipeline
+ expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
+ expect(pipeline.source_bridge).to eq bridge
+ expect(pipeline.source_bridge).to be_a ::Ci::Bridge
+ end
+ end
+
it_behaves_like 'logs downstream pipeline creation' do
+ let(:downstream_pipeline) { pipeline }
let(:expected_root_pipeline) { upstream_pipeline }
let(:expected_hierarchy_size) { 2 }
let(:expected_downstream_relationship) { :multi_project }
end
it 'updates bridge status when downstream pipeline gets processed' do
- pipeline = subject
-
expect(pipeline.reload).to be_created
expect(bridge.reload).to be_success
end
- context 'when bridge job has already any downstream pipelines' do
+ it 'triggers the upstream pipeline duration calculation', :sidekiq_inline do
+ expect { subject }
+ .to change { upstream_pipeline.reload.duration }.from(nil).to(an_instance_of(Integer))
+ end
+
+ context 'when bridge job has already any downstream pipeline' do
before do
- bridge.sourced_pipelines.create!(
+ bridge.create_sourced_pipeline!(
source_pipeline: bridge.pipeline,
source_project: bridge.project,
project: bridge.project,
@@ -147,7 +176,33 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
bridge_id: bridge.id, project_id: bridge.project.id)
.and_call_original
expect(Ci::CreatePipelineService).not_to receive(:new)
- expect(subject).to eq({ message: "Already has a downstream pipeline", status: :error })
+ expect(subject).to be_error
+ expect(subject.message).to eq("Already has a downstream pipeline")
+ end
+
+ context 'when ci_bridge_remove_sourced_pipelines is disabled' do
+ include_context 'when ci_bridge_remove_sourced_pipelines is disabled'
+
+ before do
+ bridge.sourced_pipelines.create!(
+ source_pipeline: bridge.pipeline,
+ source_project: bridge.project,
+ project: bridge.project,
+ pipeline: create(:ci_pipeline, project: bridge.project)
+ )
+ end
+
+ it 'logs an error and exits' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(
+ instance_of(described_class::DuplicateDownstreamPipelineError),
+ bridge_id: bridge.id, project_id: bridge.project.id)
+ .and_call_original
+ expect(Ci::CreatePipelineService).not_to receive(:new)
+ expect(subject).to be_error
+ expect(subject.message).to eq("Already has a downstream pipeline")
+ end
end
end
@@ -157,8 +212,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
it 'is using default branch name' do
- pipeline = subject
-
expect(pipeline.ref).to eq 'master'
end
end
@@ -171,22 +224,33 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates only one new pipeline' do
expect { subject }
.to change { Ci::Pipeline.count }.by(1)
+ expect(subject).to be_error
+ expect(subject.message).to match_array(["jobs job config should implement a script: or a trigger: keyword"])
end
it 'creates a new pipeline in a downstream project' do
- pipeline = subject
-
expect(pipeline.user).to eq bridge.user
expect(pipeline.project).to eq downstream_project
- expect(bridge.sourced_pipelines.first.pipeline).to eq pipeline
+ expect(bridge.reload.sourced_pipeline.pipeline).to eq pipeline
expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
expect(pipeline.source_bridge).to eq bridge
expect(pipeline.source_bridge).to be_a ::Ci::Bridge
end
- it 'updates the bridge status when downstream pipeline gets processed' do
- pipeline = subject
+ context 'when ci_bridge_remove_sourced_pipelines is disabled' do
+ include_context 'when ci_bridge_remove_sourced_pipelines is disabled'
+
+ it 'creates a new pipeline in a downstream project' do
+ expect(pipeline.user).to eq bridge.user
+ expect(pipeline.project).to eq downstream_project
+ expect(bridge.sourced_pipelines.first.pipeline).to eq pipeline
+ expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
+ expect(pipeline.source_bridge).to eq bridge
+ expect(pipeline.source_bridge).to be_a ::Ci::Bridge
+ end
+ end
+ it 'updates the bridge status when downstream pipeline gets processed' do
expect(pipeline.reload).to be_failed
expect(bridge.reload).to be_failed
end
@@ -201,6 +265,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a new pipeline' do
expect { subject }
.not_to change { Ci::Pipeline.count }
+ expect(subject).to be_error
+ expect(subject.message).to eq("Pre-conditions not met")
end
it 'changes status of the bridge build' do
@@ -222,32 +288,39 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates only one new pipeline' do
expect { subject }
.to change { Ci::Pipeline.count }.by(1)
+ expect(subject).to be_success
end
it 'creates a child pipeline in the same project' do
- pipeline = subject
- pipeline.reload
-
expect(pipeline.builds.map(&:name)).to match_array(%w[rspec echo])
expect(pipeline.user).to eq bridge.user
expect(pipeline.project).to eq bridge.project
- expect(bridge.sourced_pipelines.first.pipeline).to eq pipeline
+ expect(bridge.reload.sourced_pipeline.pipeline).to eq pipeline
expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
expect(pipeline.source_bridge).to eq bridge
expect(pipeline.source_bridge).to be_a ::Ci::Bridge
end
- it 'updates bridge status when downstream pipeline gets processed' do
- pipeline = subject
+ context 'when ci_bridge_remove_sourced_pipelines is disabled' do
+ include_context 'when ci_bridge_remove_sourced_pipelines is disabled'
+
+ it 'creates a child pipeline in the same project' do
+ expect(pipeline.builds.map(&:name)).to match_array(%w[rspec echo])
+ expect(pipeline.user).to eq bridge.user
+ expect(pipeline.project).to eq bridge.project
+ expect(bridge.sourced_pipelines.first.pipeline).to eq pipeline
+ expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
+ expect(pipeline.source_bridge).to eq bridge
+ expect(pipeline.source_bridge).to be_a ::Ci::Bridge
+ end
+ end
+ it 'updates bridge status when downstream pipeline gets processed' do
expect(pipeline.reload).to be_created
expect(bridge.reload).to be_success
end
it 'propagates parent pipeline settings to the child pipeline' do
- pipeline = subject
- pipeline.reload
-
expect(pipeline.ref).to eq(upstream_pipeline.ref)
expect(pipeline.sha).to eq(upstream_pipeline.sha)
expect(pipeline.source_sha).to eq(upstream_pipeline.source_sha)
@@ -276,6 +349,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it_behaves_like 'creates a child pipeline'
it_behaves_like 'logs downstream pipeline creation' do
+ let(:downstream_pipeline) { pipeline }
let(:expected_root_pipeline) { upstream_pipeline }
let(:expected_hierarchy_size) { 2 }
let(:expected_downstream_relationship) { :parent_child }
@@ -283,6 +357,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'updates the bridge job to success' do
expect { subject }.to change { bridge.status }.to 'success'
+ expect(subject).to be_success
end
context 'when bridge uses "depend" strategy' do
@@ -292,8 +367,9 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
}
end
- it 'does not update the bridge job status' do
- expect { subject }.not_to change { bridge.status }
+ it 'update the bridge job to running status' do
+ expect { subject }.to change { bridge.status }.from('pending').to('running')
+ expect(subject).to be_success
end
end
@@ -323,8 +399,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it_behaves_like 'creates a child pipeline'
it 'propagates the merge request to the child pipeline' do
- pipeline = subject
-
expect(pipeline.merge_request).to eq(merge_request)
expect(pipeline).to be_merge_request
end
@@ -341,11 +415,13 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates the pipeline' do
expect { subject }
.to change { Ci::Pipeline.count }.by(1)
+ expect(subject).to be_success
expect(bridge.reload).to be_success
end
it_behaves_like 'logs downstream pipeline creation' do
+ let(:downstream_pipeline) { pipeline }
let(:expected_root_pipeline) { upstream_pipeline.parent_pipeline }
let(:expected_hierarchy_size) { 3 }
let(:expected_downstream_relationship) { :parent_child }
@@ -394,6 +470,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'create the pipeline' do
expect { subject }.to change { Ci::Pipeline.count }.by(1)
+ expect(subject).to be_success
end
end
@@ -406,11 +483,10 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates a new pipeline allowing variables to be passed downstream' do
expect { subject }.to change { Ci::Pipeline.count }.by(1)
+ expect(subject).to be_success
end
it 'passes variables downstream from the bridge' do
- pipeline = subject
-
pipeline.variables.map(&:key).tap do |variables|
expect(variables).to include 'BRIDGE'
end
@@ -466,6 +542,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a new pipeline' do
expect { subject }
.not_to change { Ci::Pipeline.count }
+ expect(subject).to be_error
+ expect(subject.message).to eq("Pre-conditions not met")
end
it 'changes status of the bridge build' do
@@ -480,6 +558,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates a new pipeline' do
expect { subject }
.to change { Ci::Pipeline.count }
+ expect(subject).to be_success
end
it 'expect bridge build not to be failed' do
@@ -559,18 +638,16 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates only one new pipeline' do
expect { subject }
.to change { Ci::Pipeline.count }.by(1)
+ expect(subject).to be_error
+ expect(subject.message).to match_array(["jobs invalid config should implement a script: or a trigger: keyword"])
end
it 'creates a new pipeline in the downstream project' do
- pipeline = subject
-
expect(pipeline.user).to eq bridge.user
expect(pipeline.project).to eq downstream_project
end
it 'drops the bridge' do
- pipeline = subject
-
expect(pipeline.reload).to be_failed
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
@@ -585,15 +662,10 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
bridge.drop!
end
- it 'tracks the exception' do
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(
- instance_of(Ci::Bridge::InvalidTransitionError),
- bridge_id: bridge.id,
- downstream_pipeline_id: kind_of(Numeric))
-
- subject
+ it 'returns the error' do
+ expect { subject }.not_to change(downstream_project.ci_pipelines, :count)
+ expect(subject).to be_error
+ expect(subject.message).to eq('Can not run the bridge')
end
end
@@ -603,8 +675,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
it 'passes bridge variables to downstream pipeline' do
- pipeline = subject
-
expect(pipeline.variables.first)
.to have_attributes(key: 'BRIDGE', value: 'var')
end
@@ -616,8 +686,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
it 'does not pass pipeline variables directly downstream' do
- pipeline = subject
-
pipeline.variables.map(&:key).tap do |variables|
expect(variables).not_to include 'PIPELINE_VARIABLE'
end
@@ -629,8 +697,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
it 'makes it possible to pass pipeline variable downstream' do
- pipeline = subject
-
pipeline.variables.find_by(key: 'BRIDGE').tap do |variable|
expect(variable.value).to eq 'my-value-var'
end
@@ -644,11 +710,11 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a new pipeline' do
expect { subject }
.not_to change { Ci::Pipeline.count }
+ expect(subject).to be_error
+ expect(subject.message).to match_array(["Insufficient permissions to set pipeline variables"])
end
it 'ignores variables passed downstream from the bridge' do
- pipeline = subject
-
pipeline.variables.map(&:key).tap do |variables|
expect(variables).not_to include 'BRIDGE'
end
@@ -668,7 +734,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
# TODO: Move this context into a feature spec that uses
# multiple pipeline processing services. Location TBD in:
# https://gitlab.com/gitlab-org/gitlab/issues/36216
- context 'when configured with bridge job rules' do
+ context 'when configured with bridge job rules', :sidekiq_inline do
before do
stub_ci_pipeline_yaml_file(config)
downstream_project.add_maintainer(upstream_project.first_owner)
@@ -701,6 +767,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates the downstream pipeline' do
expect { subject }
.to change(downstream_project.ci_pipelines, :count).by(1)
+ expect(subject).to be_error
+ expect(subject.message).to eq("Already has a downstream pipeline")
end
end
end
@@ -731,6 +799,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a pipeline and drops the bridge' do
expect { subject }.not_to change(downstream_project.ci_pipelines, :count)
+ expect(subject).to be_error
+ expect(subject.message).to match_array(["Reference not found"])
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
@@ -754,6 +824,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a pipeline and drops the bridge' do
expect { subject }.not_to change(downstream_project.ci_pipelines, :count)
+ expect(subject).to be_error
+ expect(subject.message).to match_array(["No stages / jobs for this pipeline."])
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
@@ -776,6 +848,10 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates the pipeline but drops the bridge' do
expect { subject }.to change(downstream_project.ci_pipelines, :count).by(1)
+ expect(subject).to be_error
+ expect(subject.message).to eq(
+ ["test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post"]
+ )
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
@@ -808,6 +884,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates the pipeline' do
expect { subject }.to change(downstream_project.ci_pipelines, :count).by(1)
+ expect(subject).to be_success
expect(bridge.reload).to be_success
end
@@ -822,6 +899,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
context 'when a downstream pipeline has sibling pipelines' do
it_behaves_like 'logs downstream pipeline creation' do
+ let(:downstream_pipeline) { pipeline }
let(:expected_root_pipeline) { upstream_pipeline }
let(:expected_downstream_relationship) { :multi_project }
@@ -839,23 +917,47 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
let_it_be(:child) { create(:ci_pipeline, child_of: parent) }
let_it_be(:sibling) { create(:ci_pipeline, child_of: parent) }
- before do
- stub_const("#{described_class}::MAX_HIERARCHY_SIZE", 3)
- end
-
+ let(:project) { build(:project, :repository) }
let(:bridge) do
- create(:ci_bridge, status: :pending, user: user, options: trigger, pipeline: child)
+ create(:ci_bridge, status: :pending, user: user, options: trigger, pipeline: child, project: project)
end
- it 'does not create a new pipeline' do
- expect { subject }.not_to change { Ci::Pipeline.count }
+ context 'when limit was specified by admin' do
+ before do
+ project.actual_limits.update!(pipeline_hierarchy_size: 3)
+ end
+
+ it 'does not create a new pipeline' do
+ expect { subject }.not_to change { Ci::Pipeline.count }
+ end
+
+ it 'drops the trigger job with an explanatory reason' do
+ subject
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq('reached_max_pipeline_hierarchy_size')
+ end
end
- it 'drops the trigger job with an explanatory reason' do
- subject
+ context 'when there was no limit specified by admin' do
+ before do
+ allow(bridge.pipeline).to receive(:complete_hierarchy_count).and_return(1000)
+ end
- expect(bridge.reload).to be_failed
- expect(bridge.failure_reason).to eq('reached_max_pipeline_hierarchy_size')
+ context 'when pipeline count reaches the default limit of 1000' do
+ it 'does not create a new pipeline' do
+ expect { subject }.not_to change { Ci::Pipeline.count }
+ expect(subject).to be_error
+ expect(subject.message).to eq("Pre-conditions not met")
+ end
+
+ it 'drops the trigger job with an explanatory reason' do
+ subject
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq('reached_max_pipeline_hierarchy_size')
+ end
+ end
end
context 'with :ci_limit_complete_hierarchy_size disabled' do
@@ -865,6 +967,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates a new pipeline' do
expect { subject }.to change { Ci::Pipeline.count }.by(1)
+ expect(subject).to be_success
end
it 'marks the bridge job as successful' do
diff --git a/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb
index 74d3534eb45..0d5017a763f 100644
--- a/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb
+++ b/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute', :yaml_processor_feature_fl
pipeline = create_pipeline!
test = pipeline.statuses.find_by(name: 'instrumentation_test')
- expect(test).to be_pending
+ expect(test).to be_running
expect(pipeline.triggered_pipelines.count).to eq(1)
end
diff --git a/spec/services/ci/create_pipeline_service/environment_spec.rb b/spec/services/ci/create_pipeline_service/environment_spec.rb
index 438cb6ac895..b713cad2cad 100644
--- a/spec/services/ci/create_pipeline_service/environment_spec.rb
+++ b/spec/services/ci/create_pipeline_service/environment_spec.rb
@@ -46,6 +46,24 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
end
end
+ context 'when branch pipeline creates a dynamic environment' do
+ before do
+ config = YAML.dump(
+ review_app: {
+ script: 'echo',
+ environment: { name: "review/$CI_COMMIT_REF_NAME" }
+ })
+
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'does not associate merge request with the environment' do
+ is_expected.to be_created_successfully
+
+ expect(Environment.find_by_name('review/master').merge_request).to be_nil
+ end
+ end
+
context 'when variables are dependent on stage name' do
let(:config) do
<<~YAML
diff --git a/spec/services/ci/create_pipeline_service/logger_spec.rb b/spec/services/ci/create_pipeline_service/logger_spec.rb
index 3045f8e92b1..ccb15bfa684 100644
--- a/spec/services/ci/create_pipeline_service/logger_spec.rb
+++ b/spec/services/ci/create_pipeline_service/logger_spec.rb
@@ -2,8 +2,10 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
- context 'pipeline logger' do
+RSpec.describe Ci::CreatePipelineService, # rubocop: disable RSpec/FilePath
+ :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration do
+ describe 'pipeline logger' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
@@ -12,17 +14,11 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
let(:pipeline) { service.execute(:push).payload }
let(:file_location) { 'spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml' }
- before do
- stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
- end
-
let(:counters) do
{
'count' => a_kind_of(Numeric),
- 'avg' => a_kind_of(Numeric),
- 'sum' => a_kind_of(Numeric),
'max' => a_kind_of(Numeric),
- 'min' => a_kind_of(Numeric)
+ 'sum' => a_kind_of(Numeric)
}
end
@@ -34,15 +30,22 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
'pipeline_persisted' => true,
'project_id' => project.id,
'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
- 'pipeline_creation_duration_s' => counters,
- 'pipeline_size_count' => counters,
- 'pipeline_step_gitlab_ci_pipeline_chain_seed_duration_s' => counters,
+ 'pipeline_creation_duration_s' => a_kind_of(Numeric),
+ 'pipeline_size_count' => a_kind_of(Numeric),
+ 'pipeline_step_gitlab_ci_pipeline_chain_seed_duration_s' => a_kind_of(Numeric),
'pipeline_seed_build_inclusion_duration_s' => counters,
+ 'pipeline_seed_build_errors_duration_s' => counters,
+ 'pipeline_seed_build_to_resource_duration_s' => counters,
+ 'pipeline_seed_stage_seeds_duration_s' => counters,
'pipeline_builds_tags_count' => a_kind_of(Numeric),
'pipeline_builds_distinct_tags_count' => a_kind_of(Numeric)
}
end
+ before do
+ stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
+ end
+
context 'when the duration is under the threshold' do
it 'does not create a log entry but it collects the data' do
expect(Gitlab::AppJsonLogger).not_to receive(:info)
@@ -51,9 +54,9 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
expect(service.logger.observations_hash)
.to match(
a_hash_including(
- 'pipeline_creation_duration_s' => counters,
- 'pipeline_size_count' => counters,
- 'pipeline_step_gitlab_ci_pipeline_chain_seed_duration_s' => counters
+ 'pipeline_creation_duration_s' => a_kind_of(Numeric),
+ 'pipeline_size_count' => a_kind_of(Numeric),
+ 'pipeline_step_gitlab_ci_pipeline_chain_seed_duration_s' => a_kind_of(Numeric)
)
)
end
@@ -62,7 +65,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
context 'when the durations exceeds the threshold' do
let(:timer) do
proc do
- @timer = @timer.to_i + 30
+ @timer = @timer.to_i + 30 # rubocop: disable RSpec/InstanceVariable
end
end
@@ -88,17 +91,15 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
'pipeline_persisted' => false,
'project_id' => project.id,
'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
- 'pipeline_step_gitlab_ci_pipeline_chain_seed_duration_s' => counters
+ 'pipeline_step_gitlab_ci_pipeline_chain_seed_duration_s' => a_kind_of(Numeric)
}
end
- before do
+ it 'creates a log entry' do
allow_next_instance_of(Ci::Pipeline) do |pipeline|
expect(pipeline).to receive(:save!).and_raise { RuntimeError }
end
- end
- it 'creates a log entry' do
expect(Gitlab::AppJsonLogger)
.to receive(:info)
.with(a_hash_including(loggable_data))
@@ -125,7 +126,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
context 'when the size exceeds the threshold' do
before do
allow_next_instance_of(Ci::Pipeline) do |pipeline|
- allow(pipeline).to receive(:total_size) { 5000 }
+ allow(pipeline).to receive(:total_size).and_return(5000)
end
end
diff --git a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb
index 513cbbed6cd..eb17935967c 100644
--- a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb
+++ b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute', :yaml_processor_feature_fl
pipeline = create_pipeline!
test = pipeline.statuses.find_by(name: 'instrumentation_test')
- expect(test).to be_pending
+ expect(test).to be_running
expect(pipeline.triggered_pipelines.count).to eq(1)
end
diff --git a/spec/services/ci/create_pipeline_service/partitioning_spec.rb b/spec/services/ci/create_pipeline_service/partitioning_spec.rb
index f34d103d965..a87135cefdd 100644
--- a/spec/services/ci/create_pipeline_service/partitioning_spec.rb
+++ b/spec/services/ci/create_pipeline_service/partitioning_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, :aggregate_failures,
-:ci_partitionable do
+:ci_partitionable, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
@@ -15,8 +15,13 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
- test
- deploy
+ needs:build:
+ stage: build
+ script: echo "needs..."
+
build:
stage: build
+ needs: ["needs:build"]
script: make build
test:
@@ -95,6 +100,12 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
expect(pipeline.variables.size).to eq(2)
expect(variables_partition_ids).to eq([current_partition_id])
end
+
+ it 'assigns partition_id to needs' do
+ needs = find_need('build')
+
+ expect(needs.partition_id).to eq(current_partition_id)
+ end
end
context 'with parent child pipelines' do
@@ -144,4 +155,12 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
.find { |job| job.name == name }
.metadata
end
+
+ def find_need(name)
+ pipeline
+ .processables
+ .find { |job| job.name == name }
+ .needs
+ .first
+ 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 5fdefb2b306..b866293393b 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -912,7 +912,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
context 'when outside freeze period' do
it 'creates two jobs' do
- Timecop.freeze(2020, 4, 10, 22, 59) do
+ travel_to(Time.utc(2020, 4, 10, 22, 59)) do
expect(pipeline).to be_persisted
expect(build_names).to contain_exactly('test-job', 'deploy-job')
end
@@ -921,7 +921,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
context 'when inside freeze period' do
it 'creates one job' do
- Timecop.freeze(2020, 4, 10, 23, 1) do
+ travel_to(Time.utc(2020, 4, 10, 23, 1)) do
expect(pipeline).to be_persisted
expect(build_names).to contain_exactly('test-job')
end
@@ -946,7 +946,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
context 'when outside freeze period' do
it 'creates two jobs' do
- Timecop.freeze(2020, 4, 10, 22, 59) do
+ travel_to(Time.utc(2020, 4, 10, 22, 59)) do
expect(pipeline).to be_persisted
expect(build_names).to contain_exactly('deploy-job')
end
@@ -955,7 +955,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
context 'when inside freeze period' do
it 'does not create the pipeline', :aggregate_failures do
- Timecop.freeze(2020, 4, 10, 23, 1) do
+ travel_to(Time.utc(2020, 4, 10, 23, 1)) do
expect(response).to be_error
expect(pipeline).not_to be_persisted
end
diff --git a/spec/services/ci/create_pipeline_service/scripts_spec.rb b/spec/services/ci/create_pipeline_service/scripts_spec.rb
new file mode 100644
index 00000000000..50b558e505a
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/scripts_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.first_owner }
+
+ let(:service) { described_class.new(project, user, { ref: 'master' }) }
+ let(:pipeline) { service.execute(:push).payload }
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'when job has script and nested before_script and after_script' do
+ let(:config) do
+ <<-CI_CONFIG
+ default:
+ before_script: echo 'hello default before_script'
+ after_script: echo 'hello default after_script'
+
+ job:
+ before_script: echo 'hello job before_script'
+ after_script: echo 'hello job after_script'
+ script: echo 'hello job script'
+ CI_CONFIG
+ end
+
+ it 'creates a job with script data' do
+ expect(pipeline).to be_created_successfully
+ expect(pipeline.builds.first).to have_attributes(
+ name: 'job',
+ stage: 'test',
+ options: { script: ["echo 'hello job script'"],
+ before_script: ["echo 'hello job before_script'"],
+ after_script: ["echo 'hello job after_script'"] }
+ )
+ end
+ end
+
+ context 'when job has hooks and default hooks' do
+ let(:config) do
+ <<-CI_CONFIG
+ default:
+ hooks:
+ pre_get_sources_script:
+ - echo 'hello default pre_get_sources_script'
+
+ job1:
+ hooks:
+ pre_get_sources_script:
+ - echo 'hello job1 pre_get_sources_script'
+ script: echo 'hello job1 script'
+
+ job2:
+ script: echo 'hello job2 script'
+
+ job3:
+ inherit:
+ default: false
+ script: echo 'hello job3 script'
+ CI_CONFIG
+ end
+
+ it 'creates jobs with hook data' do
+ expect(pipeline).to be_created_successfully
+ expect(pipeline.builds.find_by(name: 'job1')).to have_attributes(
+ name: 'job1',
+ stage: 'test',
+ options: { script: ["echo 'hello job1 script'"],
+ hooks: { pre_get_sources_script: ["echo 'hello job1 pre_get_sources_script'"] } }
+ )
+ expect(pipeline.builds.find_by(name: 'job2')).to have_attributes(
+ name: 'job2',
+ stage: 'test',
+ options: { script: ["echo 'hello job2 script'"],
+ hooks: { pre_get_sources_script: ["echo 'hello default pre_get_sources_script'"] } }
+ )
+ expect(pipeline.builds.find_by(name: 'job3')).to have_attributes(
+ name: 'job3',
+ stage: 'test',
+ options: { script: ["echo 'hello job3 script'"] }
+ )
+ end
+
+ context 'when the FF ci_hooks_pre_get_sources_script is disabled' do
+ before do
+ stub_feature_flags(ci_hooks_pre_get_sources_script: false)
+ end
+
+ it 'creates jobs without hook data' do
+ expect(pipeline).to be_created_successfully
+ expect(pipeline.builds.find_by(name: 'job1')).to have_attributes(
+ name: 'job1',
+ stage: 'test',
+ options: { script: ["echo 'hello job1 script'"] }
+ )
+ expect(pipeline.builds.find_by(name: 'job2')).to have_attributes(
+ name: 'job2',
+ stage: 'test',
+ options: { script: ["echo 'hello job2 script'"] }
+ )
+ expect(pipeline.builds.find_by(name: 'job3')).to have_attributes(
+ name: 'job3',
+ stage: 'test',
+ options: { script: ["echo 'hello job3 script'"] }
+ )
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 67c13649c6f..8628e95ba80 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -79,11 +79,11 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
let(:accepted_n_plus_ones) do
1 + # SELECT "ci_instance_variables"
- 1 + # INSERT INTO "ci_stages"
- 1 + # SELECT "ci_builds".* FROM "ci_builds"
- 1 + # INSERT INTO "ci_builds"
- 1 + # INSERT INTO "ci_builds_metadata"
- 1 # SELECT "taggings".* FROM "taggings"
+ 1 + # INSERT INTO "ci_stages"
+ 1 + # SELECT "ci_builds".* FROM "ci_builds"
+ 1 + # INSERT INTO "ci_builds"
+ 1 + # INSERT INTO "ci_builds_metadata"
+ 1 # SELECT "taggings".* FROM "taggings"
end
end
end
@@ -710,6 +710,29 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
end
end
+ context 'when the configuration includes ID tokens' do
+ it 'creates variables for the ID tokens' do
+ config = YAML.dump({
+ job_with_id_tokens: {
+ script: 'ls',
+ id_tokens: {
+ 'TEST_ID_TOKEN' => {
+ aud: 'https://gitlab.com'
+ }
+ }
+ }
+ })
+ stub_ci_pipeline_yaml_file(config)
+
+ result = execute_service.payload
+
+ expect(result).to be_persisted
+ expect(result.builds.first.id_tokens).to eq({
+ 'TEST_ID_TOKEN' => { 'aud' => 'https://gitlab.com' }
+ })
+ end
+ end
+
context 'with manual actions' do
before do
config = YAML.dump({ deploy: { script: 'ls', when: 'manual' } })
diff --git a/spec/services/ci/enqueue_job_service_spec.rb b/spec/services/ci/enqueue_job_service_spec.rb
new file mode 100644
index 00000000000..c2bb0bb2bb5
--- /dev/null
+++ b/spec/services/ci/enqueue_job_service_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::EnqueueJobService, '#execute', feature_category: :continuous_integration do
+ let_it_be(:project) { create(:project) }
+ let(:user) { create(:user, developer_projects: [project]) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+
+ let(:service) do
+ described_class.new(build, current_user: user)
+ end
+
+ subject(:execute) { service.execute }
+
+ it 'assigns the user to the job' do
+ expect { execute }.to change { build.reload.user }.to(user)
+ end
+
+ it 'calls enqueue!' do
+ expect(build).to receive(:enqueue!)
+ execute
+ end
+
+ it 'calls Ci::ResetSkippedJobsService' do
+ expect_next_instance_of(Ci::ResetSkippedJobsService) do |service|
+ expect(service).to receive(:execute).with(build)
+ end
+
+ execute
+ end
+
+ it 'returns the job' do
+ expect(execute).to eq(build)
+ end
+
+ context 'when variables are supplied' do
+ let(:job_variables) do
+ [{ key: 'first', secret_value: 'first' },
+ { key: 'second', secret_value: 'second' }]
+ end
+
+ let(:service) do
+ described_class.new(build, current_user: user, variables: job_variables)
+ end
+
+ it 'assigns the variables to the job' do
+ execute
+ expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second')
+ end
+ end
+
+ context 'when the job transition is invalid' do
+ let(:bridge) { create(:ci_bridge, :failed, pipeline: pipeline, project: project) }
+
+ let(:service) do
+ described_class.new(bridge, current_user: user)
+ end
+
+ it 'raises StateMachines::InvalidTransition' do
+ expect { execute }.to raise_error StateMachines::InvalidTransition
+ end
+ end
+
+ context 'when a transition block is supplied' do
+ let(:bridge) { create(:ci_bridge, :playable, pipeline: pipeline) }
+
+ let(:service) do
+ described_class.new(bridge, current_user: user)
+ end
+
+ subject(:execute) { service.execute(&:pending!) }
+
+ it 'calls the transition block instead of enqueue!' do
+ expect(bridge).to receive(:pending!)
+ expect(bridge).not_to receive(:enqueue!)
+ execute
+ end
+ end
+end
diff --git a/spec/services/ci/generate_kubeconfig_service_spec.rb b/spec/services/ci/generate_kubeconfig_service_spec.rb
index bfde39780dd..c0858b0f0c9 100644
--- a/spec/services/ci/generate_kubeconfig_service_spec.rb
+++ b/spec/services/ci/generate_kubeconfig_service_spec.rb
@@ -4,52 +4,98 @@ require 'spec_helper'
RSpec.describe Ci::GenerateKubeconfigService do
describe '#execute' do
- let(:project) { create(:project) }
- let(:build) { create(:ci_build, project: project) }
- let(:pipeline) { build.pipeline }
- let(:agent1) { create(:cluster_agent, project: project) }
- let(:agent2) { create(:cluster_agent) }
- let(:authorization1) { create(:agent_project_authorization, agent: agent1) }
- let(:authorization2) { create(:agent_project_authorization, agent: agent2) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let_it_be(:build) { create(:ci_build, project: project, pipeline: pipeline) }
- let(:template) { instance_double(Gitlab::Kubernetes::Kubeconfig::Template) }
+ let_it_be(:agent_project) { create(:project, group: group, name: 'project-containing-agent-config') }
- subject { described_class.new(pipeline, token: build.token).execute }
+ let_it_be(:project_agent_authorization) do
+ agent = create(:cluster_agent, project: agent_project)
+ create(:agent_project_authorization, agent: agent, project: project)
+ end
+
+ let_it_be(:group_agent_authorization) do
+ agent = create(:cluster_agent, project: agent_project)
+ create(:agent_group_authorization, agent: agent, group: group)
+ end
+
+ let(:template) do
+ instance_double(
+ Gitlab::Kubernetes::Kubeconfig::Template,
+ add_cluster: nil,
+ add_user: nil,
+ add_context: nil
+ )
+ end
+
+ let(:agent_authorizations) { [project_agent_authorization, group_agent_authorization] }
+ let(:filter_service) do
+ instance_double(
+ ::Clusters::Agents::FilterAuthorizationsService,
+ execute: agent_authorizations
+ )
+ end
+
+ subject(:execute) { described_class.new(pipeline, token: build.token, environment: nil).execute }
before do
- expect(Gitlab::Kubernetes::Kubeconfig::Template).to receive(:new).and_return(template)
- expect(pipeline).to receive(:cluster_agent_authorizations).and_return([authorization1, authorization2])
+ allow(Gitlab::Kubernetes::Kubeconfig::Template).to receive(:new).and_return(template)
+ allow(::Clusters::Agents::FilterAuthorizationsService).to receive(:new).and_return(filter_service)
+ end
+
+ it 'returns a Kubeconfig Template' do
+ expect(execute).to eq(template)
end
- it 'adds a cluster, and a user and context for each available agent' do
+ it 'adds a cluster' do
expect(template).to receive(:add_cluster).with(
name: 'gitlab',
url: Gitlab::Kas.tunnel_url
).once
- expect(template).to receive(:add_user).with(
- name: "agent:#{agent1.id}",
- token: "ci:#{agent1.id}:#{build.token}"
- )
- expect(template).to receive(:add_user).with(
- name: "agent:#{agent2.id}",
- token: "ci:#{agent2.id}:#{build.token}"
- )
+ execute
+ end
- expect(template).to receive(:add_context).with(
- name: "#{project.full_path}:#{agent1.name}",
- namespace: 'production',
- cluster: 'gitlab',
- user: "agent:#{agent1.id}"
- )
- expect(template).to receive(:add_context).with(
- name: "#{agent2.project.full_path}:#{agent2.name}",
- namespace: 'production',
- cluster: 'gitlab',
- user: "agent:#{agent2.id}"
+ it "filters the pipeline's agents by `nil` environment" do
+ expect(::Clusters::Agents::FilterAuthorizationsService).to receive(:new).with(
+ pipeline.cluster_agent_authorizations,
+ environment: nil
)
- expect(subject).to eq(template)
+ execute
+ end
+
+ it 'adds user and context for all eligible agents', :aggregate_failures do
+ agent_authorizations.each do |authorization|
+ expect(template).to receive(:add_user).with(
+ name: "agent:#{authorization.agent.id}",
+ token: "ci:#{authorization.agent.id}:#{build.token}"
+ )
+
+ expect(template).to receive(:add_context).with(
+ name: "#{agent_project.full_path}:#{authorization.agent.name}",
+ namespace: 'production',
+ cluster: 'gitlab',
+ user: "agent:#{authorization.agent.id}"
+ )
+ end
+
+ execute
+ end
+
+ context 'when environment is specified' do
+ subject(:execute) { described_class.new(pipeline, token: build.token, environment: 'production').execute }
+
+ it "filters the pipeline's agents by the specified environment" do
+ expect(::Clusters::Agents::FilterAuthorizationsService).to receive(:new).with(
+ pipeline.cluster_agent_authorizations,
+ environment: 'production'
+ )
+
+ execute
+ end
end
end
end
diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
index 1fbefc1fa22..2f2af9f6c85 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do
+RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category: :continuous_integration do
describe 'Pipeline Processing Service Tests With Yaml' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/pipeline_schedules/calculate_next_run_service_spec.rb b/spec/services/ci/pipeline_schedules/calculate_next_run_service_spec.rb
new file mode 100644
index 00000000000..182c5bebbc1
--- /dev/null
+++ b/spec/services/ci/pipeline_schedules/calculate_next_run_service_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+# rubocop:disable Layout/LineLength
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineSchedules::CalculateNextRunService, feature_category: :continuous_integration do
+ let_it_be(:project) { create(:project, :public, :repository) }
+
+ describe '#execute' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:run_service) do
+ described_class.new(project).execute(pipeline_schedule,
+ fallback_method: pipeline_schedule.method(:calculate_next_run_at))
+ end
+
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: schedule_cron) }
+ let(:daily_limit_of_144_runs) { 1.day / 10.minutes }
+ let(:daily_limit_of_24_runs) { 1.day / 1.hour }
+
+ before do
+ allow(Settings).to receive(:cron_jobs) { { 'pipeline_schedule_worker' => { 'cron' => worker_cron } } }
+ create(:plan_limits, :default_plan, ci_daily_pipeline_schedule_triggers: plan_limit) if plan_limit
+ end
+
+ context "when there is invalid or no plan limits" do
+ where(:worker_cron, :schedule_cron, :plan_limit, :now, :expected_result) do
+ '0 1 2 3 *' | '0 1 * * *' | nil | Time.zone.local(2021, 3, 2, 1, 0) | Time.zone.local(2022, 3, 2, 1, 0)
+ '*/5 * * * *' | '*/1 * * * *' | nil | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 5)
+ '*/5 * * * *' | '0 * * * *' | nil | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5)
+ # 1.day / 2.hours => 12 times a day and it is invalid because there is a minimum for plan limits.
+ # See: https://docs.gitlab.com/ee/administration/instance_limits.html#limit-the-number-of-pipelines-created-by-a-pipeline-schedule-per-day
+ '*/5 * * * *' | '0 * * * *' | 1.day / 2.hours | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5)
+ end
+
+ with_them do
+ it 'calls fallback method to get next_run_at' do
+ travel_to(now) do
+ expect(pipeline_schedule).to receive(:calculate_next_run_at).and_call_original
+
+ result = run_service
+
+ expect(result).to eq(expected_result)
+ end
+ end
+ end
+ end
+
+ context "when the workers next run matches schedule's earliest run" do
+ where(:worker_cron, :schedule_cron, :plan_limit, :now, :expected_result) do
+ '*/5 * * * *' | '0 * * * *' | daily_limit_of_144_runs | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0)
+ '*/5 * * * *' | '*/5 * * * *' | daily_limit_of_144_runs | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10)
+ '*/5 * * * *' | '0 1 * * *' | daily_limit_of_144_runs | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 28, 1, 0)
+ '*/5 * * * *' | '0 2 * * *' | daily_limit_of_144_runs | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 27, 2, 0)
+ '*/5 * * * *' | '0 3 * * *' | daily_limit_of_144_runs | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 27, 3, 0)
+ '*/5 * * * *' | '0 1 1 * *' | daily_limit_of_144_runs | Time.zone.local(2021, 5, 1, 1, 0) | Time.zone.local(2021, 6, 1, 1, 0)
+ '*/9 * * * *' | '0 1 1 * *' | daily_limit_of_144_runs | Time.zone.local(2021, 5, 1, 1, 9) | Time.zone.local(2021, 6, 1, 1, 0)
+ '*/5 * * * *' | '45 21 1 2 *' | daily_limit_of_144_runs | Time.zone.local(2021, 2, 1, 21, 45) | Time.zone.local(2022, 2, 1, 21, 45)
+ end
+
+ with_them do
+ it 'calculates the next_run_at to be earliest point of match' do
+ travel_to(now) do
+ result = run_service
+
+ expect(result).to eq(expected_result)
+ end
+ end
+ end
+ end
+
+ context "when next_run_at is restricted by plan limit" do
+ where(:worker_cron, :schedule_cron, :plan_limit, :now, :expected_result) do
+ '*/5 * * * *' | '59 14 * * *' | daily_limit_of_24_runs | Time.zone.local(2021, 5, 1, 15, 0) | Time.zone.local(2021, 5, 2, 15, 0)
+ '*/5 * * * *' | '*/1 * * * *' | daily_limit_of_24_runs | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0)
+ '*/5 * * * *' | '*/1 * * * *' | daily_limit_of_144_runs | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10)
+ '*/5 * * * *' | '*/1 * * * *' | (1.day / 7.minutes).to_i | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10)
+ end
+
+ with_them do
+ it 'calculates the next_run_at based on next available limit' do
+ travel_to(now) do
+ result = run_service
+
+ expect(result).to eq(expected_result)
+ end
+ end
+ end
+ end
+
+ context "when next_run_at is restricted by worker's availability" do
+ where(:worker_cron, :schedule_cron, :plan_limit, :now, :expected_result) do
+ '0 1 2 3 *' | '0 1 * * *' | daily_limit_of_144_runs | Time.zone.local(2021, 3, 2, 1, 0) | Time.zone.local(2022, 3, 2, 1, 0)
+ end
+
+ with_them do
+ it 'calculates the next_run_at using worker_cron' do
+ travel_to(now) do
+ result = run_service
+
+ expect(result).to eq(expected_result)
+ end
+ end
+ end
+ end
+ end
+end
+# rubocop:enable Layout/LineLength
diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb
index 4b3e774ff3c..4946367380e 100644
--- a/spec/services/ci/pipeline_trigger_service_spec.rb
+++ b/spec/services/ci/pipeline_trigger_service_spec.rb
@@ -197,8 +197,7 @@ RSpec.describe Ci::PipelineTriggerService do
end
it_behaves_like 'logs downstream pipeline creation' do
- subject { result[:pipeline] }
-
+ let(:downstream_pipeline) { result[:pipeline] }
let(:expected_root_pipeline) { pipeline }
let(:expected_hierarchy_size) { 2 }
let(:expected_downstream_relationship) { :multi_project }
diff --git a/spec/services/ci/pipelines/add_job_service_spec.rb b/spec/services/ci/pipelines/add_job_service_spec.rb
index e735b2752d9..c62aa9506bd 100644
--- a/spec/services/ci/pipelines/add_job_service_spec.rb
+++ b/spec/services/ci/pipelines/add_job_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ci::Pipelines::AddJobService do
include ExclusiveLeaseHelpers
- let_it_be(:pipeline) { create(:ci_pipeline) }
+ let_it_be_with_reload(:pipeline) { create(:ci_pipeline) }
let(:job) { build(:ci_build) }
@@ -35,7 +35,7 @@ RSpec.describe Ci::Pipelines::AddJobService do
end
it 'assigns partition_id to job and metadata' do
- pipeline.partition_id = 123
+ pipeline.partition_id = ci_testing_partition_id
expect { execute }
.to change(job, :partition_id).to(pipeline.partition_id)
diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb
index 9301098b083..de308bb1a87 100644
--- a/spec/services/ci/process_build_service_spec.rb
+++ b/spec/services/ci/process_build_service_spec.rb
@@ -19,31 +19,25 @@ RSpec.describe Ci::ProcessBuildService, '#execute' do
end
end
- shared_context 'with ci_retry_job_fix disabled' do
- before do
- stub_feature_flags(ci_retry_job_fix: false)
- end
- end
-
context 'for single build' do
let!(:build) { create(:ci_build, *[trait].compact, :created, **conditions, pipeline: pipeline) }
- where(:trait, :conditions, :current_status, :after_status, :retry_after_status, :retry_disabled_after_status) do
- nil | { when: :on_success } | 'success' | 'pending' | 'pending' | 'pending'
- nil | { when: :on_success } | 'skipped' | 'pending' | 'pending' | 'pending'
- nil | { when: :on_success } | 'failed' | 'skipped' | 'skipped' | 'skipped'
- nil | { when: :on_failure } | 'success' | 'skipped' | 'skipped' | 'skipped'
- nil | { when: :on_failure } | 'skipped' | 'skipped' | 'skipped' | 'skipped'
- nil | { when: :on_failure } | 'failed' | 'pending' | 'pending' | 'pending'
- nil | { when: :always } | 'success' | 'pending' | 'pending' | 'pending'
- nil | { when: :always } | 'skipped' | 'pending' | 'pending' | 'pending'
- nil | { when: :always } | 'failed' | 'pending' | 'pending' | 'pending'
- :actionable | { when: :manual } | 'success' | 'manual' | 'pending' | 'manual'
- :actionable | { when: :manual } | 'skipped' | 'manual' | 'pending' | 'manual'
- :actionable | { when: :manual } | 'failed' | 'skipped' | 'skipped' | 'skipped'
- :schedulable | { when: :delayed } | 'success' | 'scheduled' | 'pending' | 'scheduled'
- :schedulable | { when: :delayed } | 'skipped' | 'scheduled' | 'pending' | 'scheduled'
- :schedulable | { when: :delayed } | 'failed' | 'skipped' | 'skipped' | 'skipped'
+ where(:trait, :conditions, :current_status, :after_status, :retry_after_status) do
+ nil | { when: :on_success } | 'success' | 'pending' | 'pending'
+ nil | { when: :on_success } | 'skipped' | 'pending' | 'pending'
+ nil | { when: :on_success } | 'failed' | 'skipped' | 'skipped'
+ nil | { when: :on_failure } | 'success' | 'skipped' | 'skipped'
+ nil | { when: :on_failure } | 'skipped' | 'skipped' | 'skipped'
+ nil | { when: :on_failure } | 'failed' | 'pending' | 'pending'
+ nil | { when: :always } | 'success' | 'pending' | 'pending'
+ nil | { when: :always } | 'skipped' | 'pending' | 'pending'
+ nil | { when: :always } | 'failed' | 'pending' | 'pending'
+ :actionable | { when: :manual } | 'success' | 'manual' | 'pending'
+ :actionable | { when: :manual } | 'skipped' | 'manual' | 'pending'
+ :actionable | { when: :manual } | 'failed' | 'skipped' | 'skipped'
+ :schedulable | { when: :delayed } | 'success' | 'scheduled' | 'pending'
+ :schedulable | { when: :delayed } | 'skipped' | 'scheduled' | 'pending'
+ :schedulable | { when: :delayed } | 'failed' | 'skipped' | 'skipped'
end
with_them do
@@ -57,14 +51,6 @@ RSpec.describe Ci::ProcessBuildService, '#execute' do
it 'updates the job status to retry_after_status' do
expect { subject }.to change { build.status }.to(retry_after_status)
end
-
- context 'when feature flag ci_retry_job_fix is disabled' do
- include_context 'with ci_retry_job_fix disabled'
-
- it "updates the job status to retry_disabled_after_status" do
- expect { subject }.to change { build.status }.to(retry_disabled_after_status)
- end
- end
end
end
end
@@ -84,15 +70,15 @@ RSpec.describe Ci::ProcessBuildService, '#execute' do
let!(:other_build) { create(:ci_build, :created, when: :on_success, pipeline: pipeline) }
- where(:trait, :build_when, :current_status, :after_status, :retry_after_status, :retry_disabled_after_status) do
- nil | :on_success | 'success' | 'pending' | 'pending' | 'pending'
- nil | :on_success | 'skipped' | 'skipped' | 'skipped' | 'skipped'
- nil | :manual | 'success' | 'manual' | 'pending' | 'manual'
- nil | :manual | 'skipped' | 'skipped' | 'skipped' | 'skipped'
- nil | :delayed | 'success' | 'manual' | 'pending' | 'manual'
- nil | :delayed | 'skipped' | 'skipped' | 'skipped' | 'skipped'
- :schedulable | :delayed | 'success' | 'scheduled' | 'pending' | 'scheduled'
- :schedulable | :delayed | 'skipped' | 'skipped' | 'skipped' | 'skipped'
+ where(:trait, :build_when, :current_status, :after_status, :retry_after_status) do
+ nil | :on_success | 'success' | 'pending' | 'pending'
+ nil | :on_success | 'skipped' | 'skipped' | 'skipped'
+ nil | :manual | 'success' | 'manual' | 'pending'
+ nil | :manual | 'skipped' | 'skipped' | 'skipped'
+ nil | :delayed | 'success' | 'manual' | 'pending'
+ nil | :delayed | 'skipped' | 'skipped' | 'skipped'
+ :schedulable | :delayed | 'success' | 'scheduled' | 'pending'
+ :schedulable | :delayed | 'skipped' | 'skipped' | 'skipped'
end
with_them do
@@ -106,14 +92,6 @@ RSpec.describe Ci::ProcessBuildService, '#execute' do
it 'updates the job status to retry_after_status' do
expect { subject }.to change { build.status }.to(retry_after_status)
end
-
- context 'when feature flag ci_retry_job_fix is disabled' do
- include_context 'with ci_retry_job_fix disabled'
-
- it "updates the job status to retry_disabled_after_status" do
- expect { subject }.to change { build.status }.to(retry_disabled_after_status)
- end
- end
end
end
end
diff --git a/spec/services/ci/after_requeue_job_service_spec.rb b/spec/services/ci/reset_skipped_jobs_service_spec.rb
index e6f46fb9ebe..712a21e665b 100644
--- a/spec/services/ci/after_requeue_job_service_spec.rb
+++ b/spec/services/ci/reset_skipped_jobs_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
+RSpec.describe Ci::ResetSkippedJobsService, :sidekiq_inline, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :empty_repo) }
let_it_be(:user) { project.first_owner }
@@ -12,9 +12,9 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
subject(:service) { described_class.new(project, user) }
- context 'stage-dag mixed pipeline' do
+ context 'with a stage-dag mixed pipeline' do
let(:config) do
- <<-EOY
+ <<-YAML
stages: [a, b, c]
a1:
@@ -49,7 +49,7 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
c2:
stage: c
script: exit 0
- EOY
+ YAML
end
let(:pipeline) do
@@ -150,9 +150,9 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
end
end
- context 'stage-dag mixed pipeline with some same-stage needs' do
+ context 'with stage-dag mixed pipeline with some same-stage needs' do
let(:config) do
- <<-EOY
+ <<-YAML
stages: [a, b, c]
a1:
@@ -181,7 +181,7 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
c2:
stage: c
script: exit 0
- EOY
+ YAML
end
let(:pipeline) do
@@ -239,7 +239,7 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
context 'with same-stage needs' do
let(:config) do
- <<-EOY
+ <<-YAML
a:
script: exit $(($RANDOM % 2))
@@ -250,7 +250,7 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
c:
script: exit 0
needs: [b]
- EOY
+ YAML
end
let(:pipeline) do
diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb
index 540e700efa6..c3d80f2cb56 100644
--- a/spec/services/ci/retry_job_service_spec.rb
+++ b/spec/services/ci/retry_job_service_spec.rb
@@ -48,12 +48,6 @@ RSpec.describe Ci::RetryJobService do
end
end
- shared_context 'with ci_retry_job_fix disabled' do
- before do
- stub_feature_flags(ci_retry_job_fix: false)
- end
- end
-
shared_examples_for 'clones the job' do
let(:job) { job_to_clone }
@@ -284,14 +278,6 @@ RSpec.describe Ci::RetryJobService do
with_them do
it_behaves_like 'checks enqueue_immediately?'
-
- context 'when feature flag is disabled' do
- include_context 'with ci_retry_job_fix disabled'
-
- it_behaves_like 'checks enqueue_immediately?' do
- let(:enqueue_immediately) { false }
- end
- end
end
end
end
@@ -384,15 +370,6 @@ RSpec.describe Ci::RetryJobService do
expect(subject).to be_success
expect(new_job.status).to eq after_status
end
-
- context 'when feature flag is disabled' do
- include_context 'with ci_retry_job_fix disabled'
-
- it 'enqueues the new job' do
- expect(subject).to be_success
- expect(new_job).to be_pending
- end
- end
end
end
@@ -435,15 +412,6 @@ RSpec.describe Ci::RetryJobService do
expect(subject).to be_success
expect(new_job.status).to eq after_status
end
-
- context 'when feature flag is disabled' do
- include_context 'with ci_retry_job_fix disabled'
-
- it 'enqueues the new job' do
- expect(subject).to be_success
- expect(new_job).to be_pending
- end
- end
end
end
@@ -487,19 +455,6 @@ RSpec.describe Ci::RetryJobService do
end
it_behaves_like 'checks enqueue_immediately?'
-
- context 'when feature flag is disabled' do
- include_context 'with ci_retry_job_fix disabled'
-
- it 'enqueues the new job' do
- expect(subject).to be_success
- expect(new_job).to be_pending
- end
-
- it_behaves_like 'checks enqueue_immediately?' do
- let(:enqueue_immediately) { false }
- end
- end
end
end
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 77345096537..07518c35fab 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -349,7 +349,10 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
it 'marks source bridge as pending' do
expect { service.execute(pipeline) }.to change { bridge.reload.status }.to('pending')
- .and not_change { bridge.reload.user }
+ end
+
+ it 'assigns the current user to the source bridge' do
+ expect { service.execute(pipeline) }.to change { bridge.reload.user }.to(user)
end
end
end
diff --git a/spec/services/ci/runners/assign_runner_service_spec.rb b/spec/services/ci/runners/assign_runner_service_spec.rb
index 08bb99830fb..92f6db2bdfb 100644
--- a/spec/services/ci/runners/assign_runner_service_spec.rb
+++ b/spec/services/ci/runners/assign_runner_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute' do
+RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category: :runner_fleet do
subject(:execute) { described_class.new(runner, project, user).execute }
let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
diff --git a/spec/services/ci/runners/bulk_delete_runners_service_spec.rb b/spec/services/ci/runners/bulk_delete_runners_service_spec.rb
index fa8af1100df..5e697565972 100644
--- a/spec/services/ci/runners/bulk_delete_runners_service_spec.rb
+++ b/spec/services/ci/runners/bulk_delete_runners_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::Runners::BulkDeleteRunnersService, '#execute' do
+RSpec.describe ::Ci::Runners::BulkDeleteRunnersService, '#execute', feature_category: :runner_fleet do
subject(:execute) { described_class.new(**service_args).execute }
let_it_be(:admin_user) { create(:user, :admin) }
diff --git a/spec/services/ci/runners/process_runner_version_update_service_spec.rb b/spec/services/ci/runners/process_runner_version_update_service_spec.rb
index b885138fc7a..d2a7e87b2d5 100644
--- a/spec/services/ci/runners/process_runner_version_update_service_spec.rb
+++ b/spec/services/ci/runners/process_runner_version_update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Runners::ProcessRunnerVersionUpdateService do
+RSpec.describe Ci::Runners::ProcessRunnerVersionUpdateService, feature_category: :runner_fleet do
subject(:service) { described_class.new(version) }
let(:version) { '1.0.0' }
diff --git a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb
index 1690190320a..39082b5c0f4 100644
--- a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb
+++ b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' do
+RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute', feature_category: :runner_fleet do
include RunnerReleasesHelper
subject(:execute) { described_class.new.execute }
diff --git a/spec/services/ci/runners/register_runner_service_spec.rb b/spec/services/ci/runners/register_runner_service_spec.rb
index 2d1b109072f..47d399cb19a 100644
--- a/spec/services/ci/runners/register_runner_service_spec.rb
+++ b/spec/services/ci/runners/register_runner_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
+RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute', feature_category: :runner_fleet do
let(:registration_token) { 'abcdefg123456' }
let(:token) {}
let(:args) { {} }
diff --git a/spec/services/ci/runners/reset_registration_token_service_spec.rb b/spec/services/ci/runners/reset_registration_token_service_spec.rb
index 79059712032..c8115236034 100644
--- a/spec/services/ci/runners/reset_registration_token_service_spec.rb
+++ b/spec/services/ci/runners/reset_registration_token_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::Runners::ResetRegistrationTokenService, '#execute' do
+RSpec.describe ::Ci::Runners::ResetRegistrationTokenService, '#execute', feature_category: :runner_fleet do
subject(:execute) { described_class.new(scope, current_user).execute }
let_it_be(:user) { build(:user) }
diff --git a/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb b/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
index e5cba80d567..9921f9322bd 100644
--- a/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
+++ b/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute' do
+RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', feature_category: :runner_fleet do
subject(:execute) { described_class.new(runner: runner, current_user: user, project_ids: project_ids).execute }
let_it_be(:owner_project) { create(:project) }
diff --git a/spec/services/ci/runners/unassign_runner_service_spec.rb b/spec/services/ci/runners/unassign_runner_service_spec.rb
index cf710cf6893..e91d4249473 100644
--- a/spec/services/ci/runners/unassign_runner_service_spec.rb
+++ b/spec/services/ci/runners/unassign_runner_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::Runners::UnassignRunnerService, '#execute' do
+RSpec.describe ::Ci::Runners::UnassignRunnerService, '#execute', feature_category: :runner_fleet do
let_it_be(:project) { create(:project) }
let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
diff --git a/spec/services/ci/runners/unregister_runner_service_spec.rb b/spec/services/ci/runners/unregister_runner_service_spec.rb
index 77fc299e4e1..fb779e1a673 100644
--- a/spec/services/ci/runners/unregister_runner_service_spec.rb
+++ b/spec/services/ci/runners/unregister_runner_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::Runners::UnregisterRunnerService, '#execute' do
+RSpec.describe ::Ci::Runners::UnregisterRunnerService, '#execute', feature_category: :runner_fleet do
subject(:execute) { described_class.new(runner, 'some_token').execute }
let(:runner) { create(:ci_runner) }
diff --git a/spec/services/ci/runners/update_runner_service_spec.rb b/spec/services/ci/runners/update_runner_service_spec.rb
index 1f953ac4cbb..86875df70a2 100644
--- a/spec/services/ci/runners/update_runner_service_spec.rb
+++ b/spec/services/ci/runners/update_runner_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Runners::UpdateRunnerService, '#execute' do
+RSpec.describe Ci::Runners::UpdateRunnerService, '#execute', feature_category: :runner_fleet do
subject(:execute) { described_class.new(runner).execute(params) }
let(:runner) { create(:ci_runner) }
diff --git a/spec/services/ci/test_failure_history_service_spec.rb b/spec/services/ci/test_failure_history_service_spec.rb
index c19df6e217b..10f6c6f5007 100644
--- a/spec/services/ci/test_failure_history_service_spec.rb
+++ b/spec/services/ci/test_failure_history_service_spec.rb
@@ -3,16 +3,19 @@
require 'spec_helper'
RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do
- describe '#execute' do
- let(:project) { create(:project) }
- let(:pipeline) { create(:ci_empty_pipeline, status: :created, project: project) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let_it_be_with_reload(:pipeline) do
+ create(:ci_pipeline, status: :created, project: project, ref: project.default_branch)
+ end
+ describe '#execute' do
subject(:execute_service) { described_class.new(pipeline).execute }
context 'when pipeline has failed builds with test reports' do
- before do
+ let_it_be(:job) do
# The test report has 2 unit test failures
- create(:ci_build, :failed, :test_reports, pipeline: pipeline, project: project)
+ create(:ci_build, :failed, :test_reports, pipeline: pipeline)
end
it 'creates unit test failures records' do
@@ -22,6 +25,14 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do
expect(Ci::UnitTestFailure.count).to eq(2)
end
+ it 'assigns partition_id to Ci::UnitTestFailure' do
+ execute_service
+
+ unit_test_failure_partition_ids = Ci::UnitTestFailure.distinct.pluck(:partition_id)
+
+ expect(unit_test_failure_partition_ids).to match_array([job.partition_id])
+ end
+
context 'when pipeline is not for the default branch' do
before do
pipeline.update_column(:ref, 'new-feature')
@@ -67,7 +78,7 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do
# This other test report has 1 unique unit test failure which brings us to 3 total failures across all builds
# thus exceeding the limit of 2 for MAX_TRACKABLE_FAILURES
- create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline, project: project)
+ create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline)
end
it 'does not persist data' do
@@ -82,7 +93,7 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do
context 'when test failure data have duplicates within the same payload (happens when the JUnit report has duplicate unit test names but have different failures)' do
before do
# The test report has 2 unit test failures but with the same unit test keys
- create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline, project: project)
+ create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline)
end
it 'does not fail but does not persist duplicate data' do
@@ -95,8 +106,8 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do
context 'when pipeline has no failed builds with test reports' do
before do
- create(:ci_build, :test_reports, pipeline: pipeline, project: project)
- create(:ci_build, :failed, pipeline: pipeline, project: project)
+ create(:ci_build, :test_reports, pipeline: pipeline)
+ create(:ci_build, :failed, pipeline: pipeline)
end
it 'does not persist data' do
@@ -109,14 +120,10 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do
end
describe '#should_track_failures?' do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { create(:ci_empty_pipeline, status: :created, project: project, ref: project.default_branch) }
-
subject { described_class.new(pipeline).should_track_failures? }
- before do
- create(:ci_build, :test_reports, :failed, pipeline: pipeline, project: project)
- create(:ci_build, :test_reports, :failed, pipeline: pipeline, project: project)
+ let_it_be(:jobs) do
+ create_list(:ci_build, 2, :test_reports, :failed, pipeline: pipeline)
end
context 'when feature flag is enabled and pipeline ref is the default branch' do
diff --git a/spec/services/ci/track_failed_build_service_spec.rb b/spec/services/ci/track_failed_build_service_spec.rb
index d83e56f0669..676769d2fc7 100644
--- a/spec/services/ci/track_failed_build_service_spec.rb
+++ b/spec/services/ci/track_failed_build_service_spec.rb
@@ -21,19 +21,22 @@ RSpec.describe Ci::TrackFailedBuildService do
expect(response.success?).to be true
+ context = {
+ schema: described_class::SCHEMA_URL,
+ data: {
+ build_id: build.id,
+ build_name: build.name,
+ build_artifact_types: ["sast"],
+ exit_code: exit_code,
+ failure_reason: failure_reason,
+ project: project.id
+ }
+ }
+
expect_snowplow_event(
category: 'ci::build',
action: 'failed',
- context: [{
- schema: described_class::SCHEMA_URL,
- data: {
- build_id: build.id,
- build_name: build.name,
- build_artifact_types: ["sast"],
- exit_code: exit_code,
- failure_reason: failure_reason
- }
- }],
+ context: [context],
user: user,
project: project.id)
end
diff --git a/spec/services/ci/unlock_artifacts_service_spec.rb b/spec/services/ci/unlock_artifacts_service_spec.rb
index f21afc7fe9e..c15e1cb2b5d 100644
--- a/spec/services/ci/unlock_artifacts_service_spec.rb
+++ b/spec/services/ci/unlock_artifacts_service_spec.rb
@@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe Ci::UnlockArtifactsService do
using RSpec::Parameterized::TableSyntax
- where(:tag, :ci_update_unlocked_job_artifacts) do
- false | false
- false | true
- true | false
- true | true
+ where(:tag) do
+ [
+ [false],
+ [true]
+ ]
end
with_them do
@@ -31,7 +31,6 @@ RSpec.describe Ci::UnlockArtifactsService do
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
- stub_feature_flags(ci_update_unlocked_job_artifacts: ci_update_unlocked_job_artifacts)
end
describe '#execute' do
@@ -69,17 +68,11 @@ RSpec.describe Ci::UnlockArtifactsService do
end
it 'unlocks job artifact records' do
- pending unless ci_update_unlocked_job_artifacts
-
expect { execute }.to change { ::Ci::JobArtifact.artifact_unlocked.count }.from(0).to(2)
end
it 'unlocks pipeline artifact records' do
- if ci_update_unlocked_job_artifacts
- expect { execute }.to change { ::Ci::PipelineArtifact.artifact_unlocked.count }.from(0).to(1)
- else
- expect { execute }.not_to change { ::Ci::PipelineArtifact.artifact_unlocked.count }
- end
+ expect { execute }.to change { ::Ci::PipelineArtifact.artifact_unlocked.count }.from(0).to(1)
end
end
@@ -111,17 +104,11 @@ RSpec.describe Ci::UnlockArtifactsService do
end
it 'unlocks job artifact records' do
- pending unless ci_update_unlocked_job_artifacts
-
expect { execute }.to change { ::Ci::JobArtifact.artifact_unlocked.count }.from(0).to(8)
end
it 'unlocks pipeline artifact records' do
- if ci_update_unlocked_job_artifacts
- expect { execute }.to change { ::Ci::PipelineArtifact.artifact_unlocked.count }.from(0).to(1)
- else
- expect { execute }.not_to change { ::Ci::PipelineArtifact.artifact_unlocked.count }
- end
+ expect { execute }.to change { ::Ci::PipelineArtifact.artifact_unlocked.count }.from(0).to(1)
end
end
end
diff --git a/spec/services/clusters/agents/filter_authorizations_service_spec.rb b/spec/services/clusters/agents/filter_authorizations_service_spec.rb
new file mode 100644
index 00000000000..62cff405d0c
--- /dev/null
+++ b/spec/services/clusters/agents/filter_authorizations_service_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::FilterAuthorizationsService, feature_category: :continuous_integration do
+ describe '#execute' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ let(:agent_authorizations_without_env) do
+ [
+ build(:agent_project_authorization, project: project, agent: build(:cluster_agent, project: project)),
+ build(:agent_group_authorization, group: group, agent: build(:cluster_agent, project: project)),
+ ::Clusters::Agents::ImplicitAuthorization.new(agent: build(:cluster_agent, project: project))
+ ]
+ end
+
+ let(:filter_params) { {} }
+
+ subject(:execute_filter) { described_class.new(agent_authorizations, filter_params).execute }
+
+ context 'when there are no filters' do
+ let(:agent_authorizations) { agent_authorizations_without_env }
+
+ it 'returns the authorizations as is' do
+ expect(execute_filter).to eq agent_authorizations
+ end
+ end
+
+ context 'when filtering by environment' do
+ let(:agent_authorizations_with_env) do
+ [
+ build(
+ :agent_project_authorization,
+ project: project,
+ agent: build(:cluster_agent, project: project),
+ environments: ['staging', 'review/*', 'production']
+ ),
+ build(
+ :agent_group_authorization,
+ group: group,
+ agent: build(:cluster_agent, project: project),
+ environments: ['staging', 'review/*', 'production']
+ )
+ ]
+ end
+
+ let(:agent_authorizations_with_different_env) do
+ [
+ build(
+ :agent_project_authorization,
+ project: project,
+ agent: build(:cluster_agent, project: project),
+ environments: ['staging']
+ ),
+ build(
+ :agent_group_authorization,
+ group: group,
+ agent: build(:cluster_agent, project: project),
+ environments: ['staging']
+ )
+ ]
+ end
+
+ let(:agent_authorizations) do
+ (
+ agent_authorizations_without_env +
+ agent_authorizations_with_env +
+ agent_authorizations_with_different_env
+ )
+ end
+
+ let(:filter_params) { { environment: 'production' } }
+
+ it 'returns the authorizations with the given environment AND authorizations without any environment' do
+ expected_authorizations = agent_authorizations_with_env + agent_authorizations_without_env
+
+ expect(execute_filter).to match_array expected_authorizations
+ end
+
+ context 'when environment filter has a wildcard' do
+ let(:filter_params) { { environment: 'review/123' } }
+
+ it 'returns the authorizations with matching environments AND authorizations without any environment' do
+ expected_authorizations = agent_authorizations_with_env + agent_authorizations_without_env
+
+ expect(execute_filter).to match_array expected_authorizations
+ end
+ end
+
+ context 'when environment filter is nil' do
+ let(:filter_params) { { environment: nil } }
+
+ it 'returns the authorizations without any environment' do
+ expect(execute_filter).to match_array agent_authorizations_without_env
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/agents/refresh_authorization_service_spec.rb b/spec/services/clusters/agents/refresh_authorization_service_spec.rb
index 09bec7ae0e8..fa38bc202e7 100644
--- a/spec/services/clusters/agents/refresh_authorization_service_spec.rb
+++ b/spec/services/clusters/agents/refresh_authorization_service_spec.rb
@@ -113,6 +113,16 @@ RSpec.describe Clusters::Agents::RefreshAuthorizationService do
expect(modified_authorization.config).to eq({ 'default_namespace' => 'new-namespace' })
end
+ context 'project does not belong to a group, and is in the same namespace as the agent' do
+ let(:root_ancestor) { create(:namespace) }
+ let(:added_project) { create(:project, namespace: root_ancestor) }
+
+ it 'creates an authorization record for the project' do
+ expect(subject).to be_truthy
+ expect(agent.authorized_projects).to contain_exactly(added_project)
+ end
+ end
+
context 'project does not belong to a group, and is authorizing itself' do
let(:root_ancestor) { create(:namespace) }
let(:added_project) { project }
diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb
deleted file mode 100644
index d34b4dd943c..00000000000
--- a/spec/services/clusters/applications/install_service_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::InstallService do
- describe '#execute' do
- let(:application) { create(:clusters_applications_helm, :scheduled) }
- let!(:install_command) { application.install_command }
- let(:service) { described_class.new(application) }
- let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::API) }
-
- before do
- allow(service).to receive(:install_command).and_return(install_command)
- allow(service).to receive(:helm_api).and_return(helm_client)
- end
-
- context 'when there are no errors' do
- before do
- expect(helm_client).to receive(:install).with(install_command)
- allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
- end
-
- it 'make the application installing' do
- expect(application.cluster).not_to be_nil
- service.execute
-
- expect(application).to be_installing
- end
-
- it 'schedule async installation status check' do
- expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
-
- service.execute
- end
- end
-
- context 'when k8s cluster communication fails' do
- let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) }
-
- before do
- expect(helm_client).to receive(:install).with(install_command).and_raise(error)
- end
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'Kubeclient::HttpError' }
- let(:error_message) { 'system failure' }
- let(:error_code) { 500 }
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_errored
- expect(application.status_reason).to match('Kubernetes error: 500')
- end
- end
-
- context 'a non kubernetes error happens' do
- let(:application) { create(:clusters_applications_helm, :scheduled) }
- let(:error) { StandardError.new('something bad happened') }
-
- before do
- expect(helm_client).to receive(:install).with(install_command).and_raise(error)
- end
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'StandardError' }
- let(:error_message) { 'something bad happened' }
- let(:error_code) { nil }
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_errored
- expect(application.status_reason).to eq('Failed to install.')
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/prometheus_config_service_spec.rb b/spec/services/clusters/applications/prometheus_config_service_spec.rb
deleted file mode 100644
index 7399f250248..00000000000
--- a/spec/services/clusters/applications/prometheus_config_service_spec.rb
+++ /dev/null
@@ -1,162 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::PrometheusConfigService do
- include Gitlab::Routing.url_helpers
-
- let_it_be(:project) { create(:project) }
- let_it_be(:production) { create(:environment, project: project) }
- let_it_be(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
-
- let(:application) do
- create(:clusters_applications_prometheus, :installed, cluster: cluster)
- end
-
- subject { described_class.new(project, cluster, application).execute(input) }
-
- describe '#execute' do
- let(:input) do
- YAML.load_file(Rails.root.join('vendor/prometheus/values.yaml'))
- end
-
- context 'with alerts' do
- let!(:alert) do
- create(:prometheus_alert, project: project, environment: production)
- end
-
- it 'enables alertmanager' do
- expect(subject.dig('alertmanager', 'enabled')).to eq(true)
- end
-
- describe 'alertmanagerFiles' do
- let(:alertmanager) do
- subject.dig('alertmanagerFiles', 'alertmanager.yml')
- end
-
- it 'contains receivers and route' do
- expect(alertmanager.keys).to contain_exactly('receivers', 'route')
- end
-
- describe 'receivers' do
- let(:receiver) { alertmanager.dig('receivers', 0) }
- let(:webhook_config) { receiver.dig('webhook_configs', 0) }
-
- let(:notify_url) do
- notify_project_prometheus_alerts_url(project, format: :json)
- end
-
- it 'sets receiver' do
- expect(receiver['name']).to eq('gitlab')
- end
-
- it 'sets webhook_config' do
- expect(webhook_config).to eq(
- 'url' => notify_url,
- 'send_resolved' => true,
- 'http_config' => {
- 'bearer_token' => application.alert_manager_token
- }
- )
- end
- end
-
- describe 'route' do
- let(:route) { alertmanager.fetch('route') }
-
- it 'sets route' do
- expect(route).to eq(
- 'receiver' => 'gitlab',
- 'group_wait' => '30s',
- 'group_interval' => '5m',
- 'repeat_interval' => '4h'
- )
- end
- end
- end
-
- describe 'serverFiles' do
- let(:groups) { subject.dig('serverFiles', 'alerts', 'groups') }
-
- it 'sets the alerts' do
- rules = groups.dig(0, 'rules')
- expect(rules.size).to eq(1)
-
- expect(rules.first['alert']).to eq(alert.title)
- end
-
- context 'with parameterized queries' do
- let!(:alert) do
- create(:prometheus_alert,
- project: project,
- environment: production,
- prometheus_metric: metric,
- operator: PrometheusAlert.operators['gt'],
- threshold: 0)
- end
-
- let(:metric) do
- create(:prometheus_metric, query: query, project: project)
- end
-
- let(:query) { 'up{environment="{{ci_environment_slug}}"}' }
-
- it 'substitutes query variables' do
- expect(Gitlab::Prometheus::QueryVariables)
- .to receive(:call)
- .with(production, start_time: nil, end_time: nil)
- .and_call_original
-
- expr = groups.dig(0, 'rules', 0, 'expr')
- expect(expr).to eq("up{environment=\"#{production.slug}\"} > 0.0")
- end
- end
-
- context 'with multiple environments' do
- let(:staging) { create(:environment, project: project) }
-
- before do
- create(:prometheus_alert, project: project, environment: production)
- create(:prometheus_alert, project: project, environment: staging)
- end
-
- it 'sets alerts for multiple environment' do
- env_names = groups.map { |group| group['name'] }
- expect(env_names).to contain_exactly(
- "#{production.name}.rules",
- "#{staging.name}.rules"
- )
- end
-
- it 'substitutes query variables once per environment' do
- allow(Gitlab::Prometheus::QueryVariables).to receive(:call).and_call_original
-
- expect(Gitlab::Prometheus::QueryVariables)
- .to receive(:call)
- .with(production, start_time: nil, end_time: nil)
-
- expect(Gitlab::Prometheus::QueryVariables)
- .to receive(:call)
- .with(staging, start_time: nil, end_time: nil)
-
- subject
- end
- end
- end
- end
-
- context 'without alerts' do
- it 'disables alertmanager' do
- expect(subject.dig('alertmanager', 'enabled')).to eq(false)
- end
-
- it 'removes alertmanagerFiles' do
- expect(subject).not_to include('alertmanagerFiles')
- end
-
- it 'removes alerts' do
- expect(subject.dig('serverFiles', 'alerts')).to eq({})
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/upgrade_service_spec.rb b/spec/services/clusters/applications/upgrade_service_spec.rb
deleted file mode 100644
index 22fbb7ca6e3..00000000000
--- a/spec/services/clusters/applications/upgrade_service_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::UpgradeService do
- describe '#execute' do
- let(:application) { create(:clusters_applications_helm, :scheduled) }
- let!(:install_command) { application.install_command }
- let(:service) { described_class.new(application) }
- let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::API) }
-
- before do
- allow(service).to receive(:install_command).and_return(install_command)
- allow(service).to receive(:helm_api).and_return(helm_client)
- end
-
- context 'when there are no errors' do
- before do
- expect(helm_client).to receive(:update).with(install_command)
- allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
- end
-
- it 'make the application updating' do
- expect(application.cluster).not_to be_nil
- service.execute
-
- expect(application).to be_updating
- end
-
- it 'schedule async installation status check' do
- expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
-
- service.execute
- end
- end
-
- context 'when kubernetes cluster communication fails' do
- let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) }
-
- before do
- expect(helm_client).to receive(:update).with(install_command).and_raise(error)
- end
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'Kubeclient::HttpError' }
- let(:error_message) { 'system failure' }
- let(:error_code) { 500 }
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq(_('Kubernetes error: %{error_code}') % { error_code: 500 })
- end
- end
-
- context 'a non kubernetes error happens' do
- let(:application) { create(:clusters_applications_helm, :scheduled) }
- let(:error) { StandardError.new('something bad happened') }
-
- before do
- expect(helm_client).to receive(:update).with(install_command).and_raise(error)
- end
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'StandardError' }
- let(:error_message) { 'something bad happened' }
- let(:error_code) { nil }
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq(_('Failed to upgrade.'))
- end
- end
- end
-end
diff --git a/spec/services/database/consistency_check_service_spec.rb b/spec/services/database/consistency_check_service_spec.rb
index 6695e4b5e9f..d7dee50f7c2 100644
--- a/spec/services/database/consistency_check_service_spec.rb
+++ b/spec/services/database/consistency_check_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Database::ConsistencyCheckService do
+RSpec.describe Database::ConsistencyCheckService, feature_category: :database do
let(:batch_size) { 5 }
let(:max_batches) { 2 }
diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb
index c952bcddd9a..31a3abda8c7 100644
--- a/spec/services/deployments/update_environment_service_spec.rb
+++ b/spec/services/deployments/update_environment_service_spec.rb
@@ -115,7 +115,7 @@ RSpec.describe Deployments::UpdateEnvironmentService do
let(:external_url) { 'javascript:alert("hello")' }
it 'fails to update the tier due to validation error' do
- expect { subject.execute }.not_to change { environment.tier }
+ expect { subject.execute }.not_to change { environment.reload.tier }
end
it 'tracks an exception' do
diff --git a/spec/services/environments/create_for_build_service_spec.rb b/spec/services/environments/create_for_build_service_spec.rb
index 721822f355b..c7aadb20c01 100644
--- a/spec/services/environments/create_for_build_service_spec.rb
+++ b/spec/services/environments/create_for_build_service_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Environments::CreateForBuildService do
let(:merge_request) {}
describe '#execute' do
- subject { service.execute(job, merge_request: merge_request) }
+ subject { service.execute(job) }
shared_examples_for 'returning a correct environment' do
let(:expected_auto_stop_in_seconds) do
@@ -187,10 +187,11 @@ RSpec.describe Environments::CreateForBuildService do
end
context 'when merge_request is provided' do
+ let(:pipeline) { create(:ci_pipeline, project: project, merge_request: merge_request) }
let(:environment_name) { 'development' }
let(:attributes) { { environment: environment_name, options: { environment: { name: environment_name } } } }
let(:merge_request) { create(:merge_request, source_project: project) }
- let(:seed) { described_class.new(job, merge_request: merge_request) }
+ let(:seed) { described_class.new(job) }
context 'and environment does not exist' do
let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' }
@@ -216,7 +217,8 @@ RSpec.describe Environments::CreateForBuildService do
end
context 'when a pipeline contains a deployment job' do
- let!(:job) { build(:ci_build, :start_review_app, project: project) }
+ let(:pipeline) { create(:ci_pipeline, project: project, merge_request: merge_request) }
+ let!(:job) { build(:ci_build, :start_review_app, project: project, pipeline: pipeline) }
context 'and the environment does not exist' do
it 'creates the environment specified by the job' do
diff --git a/spec/services/environments/stop_service_spec.rb b/spec/services/environments/stop_service_spec.rb
index 4f766b73710..5f983a2151a 100644
--- a/spec/services/environments/stop_service_spec.rb
+++ b/spec/services/environments/stop_service_spec.rb
@@ -204,6 +204,8 @@ RSpec.describe Environments::StopService do
context 'and merge request has associated created_environments' do
let!(:environment1) { create(:environment, project: project, merge_request: merge_request) }
let!(:environment2) { create(:environment, project: project, merge_request: merge_request) }
+ let!(:environment3) { create(:environment, project: project) }
+ let!(:environment3_deployment) { create(:deployment, environment: environment3, sha: pipeline.sha) }
before do
subject
@@ -215,8 +217,7 @@ RSpec.describe Environments::StopService do
end
it 'does not affect environments that are not associated to the merge request' do
- expect(pipeline.environments_in_self_and_project_descendants.first.merge_request).to be_nil
- expect(pipeline.environments_in_self_and_project_descendants.first).to be_available
+ expect(environment3.reload).to be_available
end
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index c3ae062a4b2..e60954a19ed 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -72,12 +72,13 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { described_class.name }
let(:action) { 'created' }
- let(:label) { 'usage_activity_by_stage_monthly.create.merge_requests_users' }
+ let(:label) { described_class::MR_EVENT_LABEL }
let(:namespace) { project.namespace }
let(:project) { merge_request.project }
let(:user) { merge_request.author }
+ let(:property) { described_class::MR_EVENT_PROPERTY }
let(:context) do
- [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: property).to_context]
end
end
end
@@ -101,12 +102,13 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { described_class.name }
let(:action) { 'closed' }
- let(:label) { 'usage_activity_by_stage_monthly.create.merge_requests_users' }
+ let(:label) { described_class::MR_EVENT_LABEL }
let(:namespace) { project.namespace }
let(:project) { merge_request.project }
let(:user) { merge_request.author }
+ let(:property) { described_class::MR_EVENT_PROPERTY }
let(:context) do
- [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: property).to_context]
end
end
end
@@ -130,12 +132,13 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { described_class.name }
let(:action) { 'merged' }
- let(:label) { 'usage_activity_by_stage_monthly.create.merge_requests_users' }
+ let(:label) { described_class::MR_EVENT_LABEL }
let(:namespace) { project.namespace }
let(:project) { merge_request.project }
let(:user) { merge_request.author }
+ let(:property) { described_class::MR_EVENT_PROPERTY }
let(:context) do
- [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: property).to_context]
end
end
end
@@ -318,10 +321,7 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:namespace) { project.namespace }
let(:feature_flag_name) { :route_hll_to_snowplow }
let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_project_repo' }
- let(:context) do
- [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
- event: 'action_active_users_project_repo').to_context]
- end
+ let(:property) { 'project_action' }
end
end
@@ -348,10 +348,7 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:namespace) { project.namespace }
let(:feature_flag_name) { :route_hll_to_snowplow }
let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_project_repo' }
- let(:context) do
- [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
- event: 'action_active_users_project_repo').to_context]
- end
+ let(:property) { 'project_action' }
end
end
@@ -408,41 +405,28 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION }
end
- it 'records correct create payload with Snowplow event' do
- service.save_designs(author, create: [design])
-
- expect_snowplow_event(
- category: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s,
- action: 'create',
- namespace: design.project.namespace,
- user: author,
- project: design.project,
- label: 'design_users'
- )
- end
-
- it 'records correct update payload with Snowplow event' do
- service.save_designs(author, update: [design])
+ describe 'Snowplow tracking' do
+ let(:project) { design.project }
+ let(:namespace) { project.namespace }
+ let(:category) { described_class.name }
+ let(:property) { Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s }
+ let(:label) { ::EventCreateService::DEGIGN_EVENT_LABEL }
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
- expect_snowplow_event(
- category: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s,
- action: 'update',
- namespace: design.project.namespace,
- user: author,
- project: design.project,
- label: 'design_users'
- )
- end
+ context 'for create event' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ subject(:design_service) { service.save_designs(author, create: [design]) }
- context 'when FF is disabled' do
- before do
- stub_feature_flags(route_hll_to_snowplow_phase2: false)
+ let(:action) { 'create' }
+ end
end
- it 'doesnt emit snowwplow events', :snowplow do
- subject
+ context 'for update event' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ subject(:design_service) { service.save_designs(author, update: [design]) }
- expect_no_snowplow_event
+ let(:action) { 'update' }
+ end
end
end
end
@@ -469,29 +453,17 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION }
end
- it 'records correct payload with Snowplow event' do
- service.destroy_designs([design], author)
-
- expect_snowplow_event(
- category: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s,
- action: 'destroy',
- namespace: design.project.namespace,
- user: author,
- project: design.project,
- label: 'design_users'
- )
- end
-
- context 'when FF is disabled' do
- before do
- stub_feature_flags(route_hll_to_snowplow_phase2: false)
- end
-
- it 'doesnt emit snowplow events', :snowplow do
- subject
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ subject(:design_service) { service.destroy_designs([design], author) }
- expect_no_snowplow_event
- end
+ let(:project) { design.project }
+ let(:namespace) { project.namespace }
+ let(:category) { described_class.name }
+ let(:action) { 'destroy' }
+ let(:user) { author }
+ let(:property) { Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s }
+ let(:label) { ::EventCreateService::DEGIGN_EVENT_LABEL }
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
end
end
end
@@ -519,12 +491,13 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:note) { create(:diff_note_on_merge_request) }
let(:category) { described_class.name }
let(:action) { 'commented' }
- let(:label) { 'usage_activity_by_stage_monthly.create.merge_requests_users' }
+ let(:property) { described_class::MR_EVENT_PROPERTY }
+ let(:label) { described_class::MR_EVENT_LABEL }
let(:namespace) { project.namespace }
let(:project) { note.project }
let(:user) { author }
let(:context) do
- [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: property).to_context]
end
end
end
diff --git a/spec/services/feature_flags/hook_service_spec.rb b/spec/services/feature_flags/hook_service_spec.rb
index 19c935e43f3..f3edaca52a9 100644
--- a/spec/services/feature_flags/hook_service_spec.rb
+++ b/spec/services/feature_flags/hook_service_spec.rb
@@ -14,14 +14,14 @@ RSpec.describe FeatureFlags::HookService do
subject(:service) { described_class.new(feature_flag, user) }
- describe 'HOOK_NAME' do
- specify { expect(described_class::HOOK_NAME).to eq(:feature_flag_hooks) }
- end
-
before do
allow(Gitlab::DataBuilder::FeatureFlag).to receive(:build).with(feature_flag, user).once.and_return(hook_data)
end
+ describe 'HOOK_NAME' do
+ specify { expect(described_class::HOOK_NAME).to eq(:feature_flag_hooks) }
+ end
+
it 'calls feature_flag.project.execute_hooks' do
expect(feature_flag.project).to receive(:execute_hooks).with(hook_data, described_class::HOOK_NAME)
diff --git a/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb b/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb
index b83037f80cd..ef77958fa60 100644
--- a/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb
+++ b/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe GoogleCloud::FetchGoogleIpListService,
- :use_clean_rails_memory_store_caching, :clean_gitlab_redis_rate_limiting do
+RSpec.describe GoogleCloud::FetchGoogleIpListService, :use_clean_rails_memory_store_caching,
+:clean_gitlab_redis_rate_limiting, feature_category: :continuous_integration do
include StubRequests
let(:google_cloud_ips) { File.read(Rails.root.join('spec/fixtures/cdn/google_cloud.json')) }
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index f2dbb69f855..2791203f395 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -8,8 +8,8 @@ RSpec.describe Groups::DestroyService do
let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:project, :repository, :legacy_storage, namespace: group) }
let!(:notification_setting) { create(:notification_setting, source: group) }
- let(:gitlab_shell) { Gitlab::Shell.new }
let(:remove_path) { group.path + "+#{group.id}+deleted" }
+ let(:removed_repo) { Gitlab::Git::Repository.new(project.repository_storage, remove_path, nil, nil) }
before do
group.add_member(user, Gitlab::Access::OWNER)
@@ -70,8 +70,11 @@ RSpec.describe Groups::DestroyService do
end
it 'verifies that paths have been deleted' do
- expect(TestEnv.storage_dir_exists?(project.repository_storage, group.path)).to be_falsey
- expect(TestEnv.storage_dir_exists?(project.repository_storage, remove_path)).to be_falsey
+ Gitlab::GitalyClient::NamespaceService.allow do
+ expect(Gitlab::GitalyClient::NamespaceService.new(project.repository_storage)
+ .exists?(group.path)).to be_falsey
+ end
+ expect(removed_repo).not_to exist
end
end
end
@@ -98,8 +101,11 @@ RSpec.describe Groups::DestroyService do
end
it 'verifies original paths and projects still exist' do
- expect(TestEnv.storage_dir_exists?(project.repository_storage, group.path)).to be_truthy
- expect(TestEnv.storage_dir_exists?(project.repository_storage, remove_path)).to be_falsey
+ Gitlab::GitalyClient::NamespaceService.allow do
+ expect(Gitlab::GitalyClient::NamespaceService.new(project.repository_storage)
+ .exists?(group.path)).to be_truthy
+ end
+ expect(removed_repo).not_to exist
expect(Project.unscoped.count).to eq(1)
expect(Group.unscoped.count).to eq(2)
end
@@ -150,7 +156,7 @@ RSpec.describe Groups::DestroyService do
let!(:project) { create(:project, :legacy_storage, :empty_repo, namespace: group) }
it 'removes repository' do
- expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
+ expect(project.repository.raw).not_to exist
end
end
@@ -158,7 +164,7 @@ RSpec.describe Groups::DestroyService do
let!(:project) { create(:project, :empty_repo, namespace: group) }
it 'removes repository' do
- expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
+ expect(project.repository.raw).not_to exist
end
end
end
diff --git a/spec/services/groups/import_export/import_service_spec.rb b/spec/services/groups/import_export/import_service_spec.rb
index 66b50704939..d41acbcc2de 100644
--- a/spec/services/groups/import_export/import_service_spec.rb
+++ b/spec/services/groups/import_export/import_service_spec.rb
@@ -148,6 +148,14 @@ RSpec.describe Groups::ImportExport::ImportService do
action: 'create',
label: 'import_group_from_file'
)
+
+ expect_snowplow_event(
+ category: 'Groups::ImportExport::ImportService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Owner', import_type: 'import_group_from_file' }
+ )
end
it 'removes import file' do
@@ -235,6 +243,14 @@ RSpec.describe Groups::ImportExport::ImportService do
)
service.execute
+
+ expect_snowplow_event(
+ category: 'Groups::ImportExport::ImportService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Owner', import_type: 'import_group_from_file' }
+ )
end
end
end
@@ -275,6 +291,14 @@ RSpec.describe Groups::ImportExport::ImportService do
action: 'create',
label: 'import_group_from_file'
)
+
+ expect_snowplow_event(
+ category: 'Groups::ImportExport::ImportService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Owner', import_type: 'import_group_from_file' }
+ )
end
it 'removes import file' do
@@ -352,6 +376,24 @@ RSpec.describe Groups::ImportExport::ImportService do
expect(service.execute).to be_truthy
end
+ it 'tracks the event' do
+ service.execute
+
+ expect_snowplow_event(
+ category: 'Groups::ImportExport::ImportService',
+ action: 'create',
+ label: 'import_group_from_file'
+ )
+
+ expect_snowplow_event(
+ category: 'Groups::ImportExport::ImportService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Owner', import_type: 'import_group_from_file' }
+ )
+ end
+
it 'logs the import success' do
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
diff --git a/spec/services/import/bitbucket_server_service_spec.rb b/spec/services/import/bitbucket_server_service_spec.rb
index 0b9fe10e95a..555812ca9cf 100644
--- a/spec/services/import/bitbucket_server_service_spec.rb
+++ b/spec/services/import/bitbucket_server_service_spec.rb
@@ -31,6 +31,25 @@ RSpec.describe Import::BitbucketServerService do
allow(subject).to receive(:authorized?).and_return(true)
end
+ context 'execute' do
+ before do
+ allow(subject).to receive(:authorized?).and_return(true)
+ allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(repo))
+ end
+
+ it 'tracks an access level event' do
+ subject.execute(credentials)
+
+ expect_snowplow_event(
+ category: 'Import::BitbucketServerService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { import_type: 'bitbucket', user_role: 'Owner' }
+ )
+ end
+ end
+
context 'when no repo is found' do
before do
allow(subject).to receive(:authorized?).and_return(true)
diff --git a/spec/services/import/github/gists_import_service_spec.rb b/spec/services/import/github/gists_import_service_spec.rb
new file mode 100644
index 00000000000..c5d73e6479d
--- /dev/null
+++ b/spec/services/import/github/gists_import_service_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Import::Github::GistsImportService, feature_category: :importer do
+ subject(:import) { described_class.new(user, params) }
+
+ let_it_be(:user) { create(:user) }
+ let(:params) { { github_access_token: 'token' } }
+ let(:import_status) { instance_double('Gitlab::GithubGistsImport::Status') }
+
+ describe '#execute', :aggregate_failures do
+ before do
+ allow(Gitlab::GithubGistsImport::Status).to receive(:new).and_return(import_status)
+ end
+
+ context 'when import in progress' do
+ let(:expected_result) do
+ {
+ status: :error,
+ http_status: 422,
+ message: 'Import already in progress'
+ }
+ end
+
+ it 'returns error' do
+ expect(import_status).to receive(:started?).and_return(true)
+ expect(import.execute).to eq(expected_result)
+ end
+ end
+
+ context 'when import was not started' do
+ it 'returns success' do
+ encrypted_token = Gitlab::CryptoHelper.aes256_gcm_encrypt(params[:github_access_token])
+ expect(import_status).to receive(:started?).and_return(false)
+ expect(Gitlab::CryptoHelper)
+ .to receive(:aes256_gcm_encrypt).with(params[:github_access_token])
+ .and_return(encrypted_token)
+ expect(Gitlab::GithubGistsImport::StartImportWorker)
+ .to receive(:perform_async).with(user.id, encrypted_token)
+ expect(import_status).to receive(:start!)
+
+ expect(import.execute).to eq({ status: :success })
+ end
+ end
+ end
+end
diff --git a/spec/services/import/github_service_spec.rb b/spec/services/import/github_service_spec.rb
index 38d84009f08..d1b372c5e87 100644
--- a/spec/services/import/github_service_spec.rb
+++ b/spec/services/import/github_service_spec.rb
@@ -82,9 +82,16 @@ RSpec.describe Import::GithubService do
end
context 'when there is no repository size limit defined' do
- it 'skips the check and succeeds' do
+ it 'skips the check, succeeds, and tracks an access level' do
expect(subject.execute(access_params, :github)).to include(status: :success)
expect(settings).to have_received(:write).with(nil)
+ expect_snowplow_event(
+ category: 'Import::GithubService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { import_type: 'github', user_role: 'Owner' }
+ )
end
end
@@ -98,6 +105,13 @@ RSpec.describe Import::GithubService do
it 'succeeds when the repository is smaller than the limit' do
expect(subject.execute(access_params, :github)).to include(status: :success)
expect(settings).to have_received(:write).with(nil)
+ expect_snowplow_event(
+ category: 'Import::GithubService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { import_type: 'github', user_role: 'Not a member' }
+ )
end
it 'returns error when the repository is larger than the limit' do
@@ -118,6 +132,13 @@ RSpec.describe Import::GithubService do
it 'succeeds when the repository is smaller than the limit' do
expect(subject.execute(access_params, :github)).to include(status: :success)
expect(settings).to have_received(:write).with(nil)
+ expect_snowplow_event(
+ category: 'Import::GithubService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { import_type: 'github', user_role: 'Owner' }
+ )
end
it 'returns error when the repository is larger than the limit' do
diff --git a/spec/services/incident_management/incidents/create_service_spec.rb b/spec/services/incident_management/incidents/create_service_spec.rb
index 851b21e1227..7db762b9c5b 100644
--- a/spec/services/incident_management/incidents/create_service_spec.rb
+++ b/spec/services/incident_management/incidents/create_service_spec.rb
@@ -66,6 +66,26 @@ RSpec.describe IncidentManagement::Incidents::CreateService do
end
end
end
+
+ context 'with an alert' do
+ subject(:create_incident) { described_class.new(project, user, title: title, description: description, alert: alert).execute }
+
+ context 'when the alert is valid' do
+ let(:alert) { create(:alert_management_alert, project: project) }
+
+ it 'associates the alert with the incident' do
+ expect(create_incident[:issue].reload.alert_management_alerts).to match_array([alert])
+ end
+ end
+
+ context 'when the alert is not valid' do
+ let(:alert) { create(:alert_management_alert, :with_validation_errors, project: project) }
+
+ it 'does not associate the alert with the incident' do
+ expect(create_incident[:issue].reload.alert_management_alerts).to be_empty
+ end
+ end
+ end
end
context 'when incident has no title' do
@@ -89,10 +109,6 @@ RSpec.describe IncidentManagement::Incidents::CreateService do
subject(:create_incident) { described_class.new(project, user, title: title, description: description, alert: alert).execute }
- it 'associates the alert with the incident' do
- expect(create_incident[:issue].alert_management_alert).to eq(alert)
- end
-
context 'the alert prevents the issue from saving' do
let(:alert) { create(:alert_management_alert, :with_validation_errors, project: project) }
diff --git a/spec/services/incident_management/link_alerts/create_service_spec.rb b/spec/services/incident_management/link_alerts/create_service_spec.rb
new file mode 100644
index 00000000000..fab28771174
--- /dev/null
+++ b/spec/services/incident_management/link_alerts/create_service_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IncidentManagement::LinkAlerts::CreateService, feature_category: :incident_management do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:another_project) { create(:project) }
+ let_it_be(:linked_alert) { create(:alert_management_alert, project: project) }
+ let_it_be(:alert1) { create(:alert_management_alert, project: project) }
+ let_it_be(:alert2) { create(:alert_management_alert, project: project) }
+ let_it_be(:external_alert) { create(:alert_management_alert, project: another_project) }
+ let_it_be(:incident) { create(:incident, project: project, alert_management_alerts: [linked_alert]) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:another_developer) { create(:user) }
+
+ before_all do
+ project.add_guest(guest)
+ project.add_developer(developer)
+ project.add_developer(another_developer)
+
+ another_project.add_guest(guest)
+ another_project.add_developer(developer)
+ end
+
+ describe '#execute' do
+ subject(:execute) { described_class.new(incident, current_user, alert_references).execute }
+
+ let(:alert_references) { [alert1.to_reference, alert2.details_url] }
+
+ context 'when current user is a guest' do
+ let(:current_user) { guest }
+
+ it 'responds with error', :aggregate_failures do
+ response = execute
+
+ expect(response).to be_error
+ expect(response.message).to eq('You have insufficient permissions to manage alerts for this project')
+ end
+
+ it 'does not link alerts to the incident' do
+ expect { execute }.not_to change { incident.reload.alert_management_alerts.to_a }
+ end
+ end
+
+ context 'when current user is a developer' do
+ let(:current_user) { developer }
+
+ it 'responds with success', :aggregate_failures do
+ response = execute
+
+ expect(response).to be_success
+ expect(response.payload[:incident]).to eq(incident)
+ end
+
+ it 'links alerts to the incident' do
+ expect { execute }
+ .to change { incident.reload.alert_management_alerts.to_a }
+ .from([linked_alert])
+ .to match_array([linked_alert, alert1, alert2])
+ end
+
+ context 'when linking an already linked alert' do
+ let(:alert_references) { [linked_alert.details_url] }
+
+ it 'does not change incident alerts list' do
+ expect { execute }.not_to change { incident.reload.alert_management_alerts.to_a }
+ end
+ end
+
+ context 'when linking an alert from another project' do
+ let(:alert_references) { [external_alert.details_url] }
+
+ it 'links an external alert to the incident' do
+ expect { execute }
+ .to change { incident.reload.alert_management_alerts.to_a }
+ .from([linked_alert])
+ .to match_array([linked_alert, external_alert])
+ end
+ end
+ end
+
+ context 'when current user does not have permission to read alerts on external project' do
+ let(:current_user) { another_developer }
+
+ context 'when linking alerts from current and external projects' do
+ let(:alert_references) { [alert1.details_url, external_alert.details_url] }
+
+ it 'links only alerts the current user can read' do
+ expect { execute }
+ .to change { incident.reload.alert_management_alerts.to_a }
+ .from([linked_alert])
+ .to match_array([linked_alert, alert1])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/incident_management/link_alerts/destroy_service_spec.rb b/spec/services/incident_management/link_alerts/destroy_service_spec.rb
new file mode 100644
index 00000000000..13885ab7d5d
--- /dev/null
+++ b/spec/services/incident_management/link_alerts/destroy_service_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IncidentManagement::LinkAlerts::DestroyService, feature_category: :incident_management do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:another_project) { create(:project) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:incident) { create(:incident, project: project) }
+ let_it_be(:another_incident) { create(:incident, project: project) }
+ let_it_be(:internal_alert) { create(:alert_management_alert, project: project, issue: incident) }
+ let_it_be(:external_alert) { create(:alert_management_alert, project: another_project, issue: incident) }
+ let_it_be(:unrelated_alert) { create(:alert_management_alert, project: project, issue: another_incident) }
+
+ before_all do
+ project.add_guest(guest)
+ project.add_developer(developer)
+ end
+
+ describe '#execute' do
+ subject(:execute) { described_class.new(incident, current_user, alert).execute }
+
+ let(:alert) { internal_alert }
+
+ context 'when current user is a guest' do
+ let(:current_user) { guest }
+
+ it 'responds with error', :aggregate_failures do
+ response = execute
+
+ expect(response).to be_error
+ expect(response.message).to eq('You have insufficient permissions to manage alerts for this project')
+ end
+
+ it 'does not unlink alert from the incident' do
+ expect { execute }.not_to change { incident.reload.alert_management_alerts.to_a }
+ end
+ end
+
+ context 'when current user is a developer' do
+ let(:current_user) { developer }
+
+ it 'responds with success', :aggregate_failures do
+ response = execute
+
+ expect(response).to be_success
+ expect(response.payload[:incident]).to eq(incident)
+ end
+
+ context 'when unlinking internal alert' do
+ let(:alert) { internal_alert }
+
+ it 'unlinks the alert' do
+ expect { execute }
+ .to change { incident.reload.alert_management_alerts.to_a }
+ .to match_array([external_alert])
+ end
+ end
+
+ context 'when unlinking external alert' do
+ let(:alert) { external_alert }
+
+ it 'unlinks the alert' do
+ expect { execute }
+ .to change { incident.reload.alert_management_alerts.to_a }
+ .to match_array([internal_alert])
+ end
+ end
+
+ context 'when unlinking an alert not related to the incident' do
+ let(:alert) { unrelated_alert }
+
+ it "does not change the incident's alerts" do
+ expect { execute }.not_to change { incident.reload.alert_management_alerts.to_a }
+ end
+
+ it "does not change another incident's alerts" do
+ expect { execute }.not_to change { another_incident.reload.alert_management_alerts.to_a }
+ end
+
+ it "does not change the alert's incident" do
+ expect { execute }.not_to change { unrelated_alert.reload.issue }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb b/spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb
index 572b1a20166..2fda789cf56 100644
--- a/spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb
+++ b/spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe IncidentManagement::PagerDuty::CreateIncidentIssueService do
let(:webhook_payload) { Gitlab::Json.parse(fixture_file('pager_duty/webhook_incident_trigger.json')) }
let(:parsed_payload) { ::PagerDuty::WebhookPayloadParser.call(webhook_payload) }
- let(:incident_payload) { parsed_payload.first['incident'] }
+ let(:incident_payload) { parsed_payload['incident'] }
subject(:execute) { described_class.new(project, incident_payload).execute }
@@ -41,14 +41,14 @@ RSpec.describe IncidentManagement::PagerDuty::CreateIncidentIssueService do
expect(execute.payload[:issue].description).to eq(
<<~MARKDOWN.chomp
- **Incident:** [My new incident](https://webdemo.pagerduty.com/incidents/PRORDTY)#{markdown_line_break}
- **Incident number:** 33#{markdown_line_break}
+ **Incident:** [[FILTERED]](https://gitlab-1.pagerduty.com/incidents/Q1XZUF87W1HB5A)#{markdown_line_break}
+ **Incident number:** 2#{markdown_line_break}
**Urgency:** high#{markdown_line_break}
**Status:** triggered#{markdown_line_break}
- **Incident key:** #{markdown_line_break}
- **Created at:** 26 September 2017, 3:14PM (UTC)#{markdown_line_break}
- **Assignees:** [Laura Haley](https://webdemo.pagerduty.com/users/P553OPV)#{markdown_line_break}
- **Impacted services:** [Production XDB Cluster](https://webdemo.pagerduty.com/services/PN49J75)
+ **Incident key:** [FILTERED]#{markdown_line_break}
+ **Created at:** 30 November 2022, 8:46AM (UTC)#{markdown_line_break}
+ **Assignees:** [Rajendra Kadam](https://gitlab-1.pagerduty.com/users/PIN0B5C)#{markdown_line_break}
+ **Impacted service:** [Test service](https://gitlab-1.pagerduty.com/services/PK6IKMT)
MARKDOWN
)
end
diff --git a/spec/services/incident_management/pager_duty/process_webhook_service_spec.rb b/spec/services/incident_management/pager_duty/process_webhook_service_spec.rb
index 8b6eb21c25d..e2aba0b61af 100644
--- a/spec/services/incident_management/pager_duty/process_webhook_service_spec.rb
+++ b/spec/services/incident_management/pager_duty/process_webhook_service_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe IncidentManagement::PagerDuty::ProcessWebhookService do
end
it 'processes issues' do
- incident_payload = ::PagerDuty::WebhookPayloadParser.call(webhook_payload).first['incident']
+ incident_payload = ::PagerDuty::WebhookPayloadParser.call(webhook_payload)['incident']
expect(::IncidentManagement::PagerDuty::ProcessIncidentWorker)
.to receive(:perform_async)
diff --git a/spec/services/incident_management/timeline_events/create_service_spec.rb b/spec/services/incident_management/timeline_events/create_service_spec.rb
index b10862a78b5..a3810879c65 100644
--- a/spec/services/incident_management/timeline_events/create_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/create_service_spec.rb
@@ -55,6 +55,15 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
end
it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_created
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { project.namespace.reload }
+ let(:category) { described_class.to_s }
+ let(:user) { current_user }
+ let(:action) { 'incident_management_timeline_event_created' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
end
subject(:execute) { service.execute }
@@ -276,6 +285,15 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_created
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { project.namespace.reload }
+ let(:category) { described_class.to_s }
+ let(:user) { current_user }
+ let(:action) { 'incident_management_timeline_event_created' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
+
it 'successfully creates a database record', :aggregate_failures do
expect { execute }.to change { ::IncidentManagement::TimelineEvent.count }.by(1)
end
diff --git a/spec/services/incident_management/timeline_events/destroy_service_spec.rb b/spec/services/incident_management/timeline_events/destroy_service_spec.rb
index e1b258960ae..f90ff72a2bf 100644
--- a/spec/services/incident_management/timeline_events/destroy_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/destroy_service_spec.rb
@@ -65,6 +65,15 @@ RSpec.describe IncidentManagement::TimelineEvents::DestroyService do
end
it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_deleted
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { project.namespace.reload }
+ let(:category) { described_class.to_s }
+ let(:user) { current_user }
+ let(:action) { 'incident_management_timeline_event_deleted' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
end
end
end
diff --git a/spec/services/incident_management/timeline_events/update_service_spec.rb b/spec/services/incident_management/timeline_events/update_service_spec.rb
index 2373a73e108..ff802109715 100644
--- a/spec/services/incident_management/timeline_events/update_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/update_service_spec.rb
@@ -2,10 +2,20 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
+RSpec.describe IncidentManagement::TimelineEvents::UpdateService, feature_category: :incident_management do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:incident) { create(:incident, project: project) }
+ let_it_be(:tag1) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 1') }
+ let_it_be(:tag2) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 2') }
+ let_it_be(:tag3) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 3') }
+
+ let!(:tag_link1) do
+ create(:incident_management_timeline_event_tag_link,
+ timeline_event: timeline_event,
+ timeline_event_tag: tag3
+ )
+ end
let!(:timeline_event) { create(:incident_management_timeline_event, project: project, incident: incident) }
let(:occurred_at) { 1.minute.ago }
@@ -13,6 +23,24 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
let(:current_user) { user }
describe '#execute' do
+ shared_examples 'successful tag response' do
+ it_behaves_like 'successful response'
+
+ it 'adds the new tag' do
+ expect { execute }.to change { timeline_event.timeline_event_tags.count }.by(1)
+ end
+
+ it 'adds the new tag link' do
+ expect { execute }.to change { IncidentManagement::TimelineEventTagLink.count }.by(1)
+ end
+
+ it 'returns the new tag in response' do
+ timeline_event = execute.payload[:timeline_event]
+
+ expect(timeline_event.timeline_event_tags.pluck_names).to contain_exactly(tag1.name, tag3.name)
+ end
+ end
+
shared_examples 'successful response' do
it 'responds with success', :aggregate_failures do
expect(execute).to be_success
@@ -20,6 +48,14 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
end
it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_edited
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { project.namespace.reload }
+ let(:category) { described_class.to_s }
+ let(:action) { 'incident_management_timeline_event_edited' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
end
shared_examples 'error response' do |message|
@@ -67,7 +103,7 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
it_behaves_like 'passing the correct was_changed value', :occurred_at_and_note
context 'when note is nil' do
- let(:params) { { occurred_at: occurred_at } }
+ let(:params) { { occurred_at: occurred_at, timeline_event_tag_names: [tag3.name, tag2.name] } }
it_behaves_like 'successful response'
it_behaves_like 'passing the correct was_changed value', :occurred_at
@@ -79,18 +115,30 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
it 'updates occurred_at' do
expect { execute }.to change { timeline_event.occurred_at }.to(params[:occurred_at])
end
+
+ it 'updates the tags' do
+ expect { execute }.to change { timeline_event.timeline_event_tags.count }.by(1)
+ end
end
context 'when note is blank' do
- let(:params) { { note: '', occurred_at: occurred_at } }
+ let(:params) { { note: '', occurred_at: occurred_at, timeline_event_tag_names: [tag3.name, tag2.name] } }
it_behaves_like 'error response', "Timeline text can't be blank"
+
+ it 'does not add the tags as it rollsback the transaction' do
+ expect { execute }.not_to change { timeline_event.timeline_event_tags.count }
+ end
end
context 'when note is more than 280 characters long' do
- let(:params) { { note: 'n' * 281, occurred_at: occurred_at } }
+ let(:params) { { note: 'n' * 281, occurred_at: occurred_at, timeline_event_tag_names: [tag3.name, tag2.name] } }
it_behaves_like 'error response', 'Timeline text is too long (maximum is 280 characters)'
+
+ it 'does not add the tags as it rollsback the transaction' do
+ expect { execute }.not_to change { timeline_event.timeline_event_tags.count }
+ end
end
context 'when occurred_at is nil' do
@@ -109,9 +157,13 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
end
context 'when occurred_at is blank' do
- let(:params) { { note: 'Updated note', occurred_at: '' } }
+ let(:params) { { note: 'Updated note', occurred_at: '', timeline_event_tag_names: [tag3.name, tag2.name] } }
it_behaves_like 'error response', "Occurred at can't be blank"
+
+ it 'does not add the tags as it rollsback the transaction' do
+ expect { execute }.not_to change { timeline_event.timeline_event_tags.count }
+ end
end
context 'when both occurred_at and note is nil' do
@@ -142,6 +194,112 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
it_behaves_like 'error response',
'You have insufficient permissions to manage timeline events for this incident'
end
+
+ context 'when timeline event tags are passed' do
+ context 'when predefined tags are passed' do
+ let(:params) do
+ {
+ note: 'Updated note',
+ occurred_at: occurred_at,
+ timeline_event_tag_names: ['start time', 'end time']
+ }
+ end
+
+ it 'returns the new tag in response' do
+ timeline_event = execute.payload[:timeline_event]
+
+ expect(timeline_event.timeline_event_tags.pluck_names).to contain_exactly('Start time', 'End time')
+ end
+
+ it 'creates the predefined tags on the project' do
+ execute
+
+ expect(project.incident_management_timeline_event_tags.pluck_names).to include('Start time', 'End time')
+ end
+ end
+
+ context 'when they exist' do
+ let(:params) do
+ {
+ note: 'Updated note',
+ occurred_at: occurred_at,
+ timeline_event_tag_names: [tag3.name, tag1.name]
+ }
+ end
+
+ it_behaves_like 'successful tag response'
+
+ context 'when tag name is of random case' do
+ let(:params) do
+ {
+ note: 'Updated note',
+ occurred_at: occurred_at,
+ timeline_event_tag_names: ['tAg 3', 'TaG 1']
+ }
+ end
+
+ it_behaves_like 'successful tag response'
+ end
+
+ context 'when tag is removed' do
+ let(:params) { { note: 'Updated note', occurred_at: occurred_at, timeline_event_tag_names: [tag2.name] } }
+
+ it_behaves_like 'successful response'
+
+ it 'adds the new tag and removes the old tag' do
+ # Since it adds a tag (+1) and removes old tag (-1) so next change in count in 0
+ expect { execute }.to change { timeline_event.timeline_event_tags.count }.by(0)
+ end
+
+ it 'adds the new tag link and removes the old tag link' do
+ # Since it adds a tag link (+1) and removes old tag link (-1) so next change in count in 0
+ expect { execute }.to change { IncidentManagement::TimelineEventTagLink.count }.by(0)
+ end
+
+ it 'returns the new tag and does not contain the old tag in response' do
+ timeline_event = execute.payload[:timeline_event]
+
+ expect(timeline_event.timeline_event_tags.pluck_names).to contain_exactly(tag2.name)
+ end
+ end
+
+ context 'when all assigned tags are removed' do
+ let(:params) { { note: 'Updated note', occurred_at: occurred_at, timeline_event_tag_names: [] } }
+
+ it_behaves_like 'successful response'
+
+ it 'removes all the assigned tags' do
+ expect { execute }.to change { timeline_event.timeline_event_tags.count }.by(-1)
+ end
+
+ it 'removes all the assigned tag links' do
+ expect { execute }.to change { IncidentManagement::TimelineEventTagLink.count }.by(-1)
+ end
+
+ it 'does not contain any tags in response' do
+ timeline_event = execute.payload[:timeline_event]
+
+ expect(timeline_event.timeline_event_tags.pluck_names).to be_empty
+ end
+ end
+ end
+
+ context 'when they do not exist' do
+ let(:params) do
+ {
+ note: 'Updated note 2',
+ occurred_at: occurred_at,
+ timeline_event_tag_names: ['non existing tag']
+ }
+ end
+
+ it_behaves_like 'error response', "Following tags don't exist: [\"non existing tag\"]"
+
+ it 'does not update the note' do
+ expect { execute }.not_to change { timeline_event.reload.note }
+ end
+ end
+ end
end
context 'when user does not have permissions' do
diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb
index 2ce47f42a72..ecdd8d031c9 100644
--- a/spec/services/issuable/discussions_list_service_spec.rb
+++ b/spec/services/issuable/discussions_list_service_spec.rb
@@ -17,6 +17,19 @@ RSpec.describe Issuable::DiscussionsListService do
let_it_be(:issuable) { create(:issue, project: project) }
it_behaves_like 'listing issuable discussions', :guest, 1, 7
+
+ context 'without notes widget' do
+ let_it_be(:issuable) { create(:work_item, :issue, project: project) }
+
+ before do
+ stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
+ stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+ end
+
+ it "returns no notes" do
+ expect(discussions_service.execute).to be_empty
+ end
+ end
end
describe 'fetching notes for merge requests' do
diff --git a/spec/services/issue_links/create_service_spec.rb b/spec/services/issue_links/create_service_spec.rb
index 9cb5980716a..88e8470658d 100644
--- a/spec/services/issue_links/create_service_spec.rb
+++ b/spec/services/issue_links/create_service_spec.rb
@@ -41,6 +41,14 @@ RSpec.describe IssueLinks::CreateService do
it_behaves_like 'an incident management tracked event', :incident_management_incident_relate do
let(:current_user) { user }
end
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { issue.namespace }
+ let(:category) { described_class.to_s }
+ let(:action) { 'incident_management_incident_relate' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
end
end
end
diff --git a/spec/services/issue_links/destroy_service_spec.rb b/spec/services/issue_links/destroy_service_spec.rb
index a478a2c1448..ecb53b5cd31 100644
--- a/spec/services/issue_links/destroy_service_spec.rb
+++ b/spec/services/issue_links/destroy_service_spec.rb
@@ -25,6 +25,14 @@ RSpec.describe IssueLinks::DestroyService do
it_behaves_like 'an incident management tracked event', :incident_management_incident_unrelate do
let(:current_user) { user }
end
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { issue_b.namespace }
+ let(:category) { described_class.to_s }
+ let(:action) { 'incident_management_incident_unrelate' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
end
end
end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index ef92b6984d5..e6ad755f911 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -99,6 +99,14 @@ RSpec.describe Issues::CloseService do
it_behaves_like 'an incident management tracked event', :incident_management_incident_closed
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { issue.namespace }
+ let(:category) { described_class.to_s }
+ let(:action) { 'incident_management_incident_closed' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
+
it 'creates a new escalation resolved escalation status', :aggregate_failures do
expect { service.execute(issue) }.to change { IncidentManagement::IssuableEscalationStatus.where(issue: issue).count }.by(1)
@@ -346,31 +354,26 @@ RSpec.describe Issues::CloseService do
context 'when there is an associated Alert Management Alert' do
context 'when alert can be resolved' do
- let!(:alert) { create(:alert_management_alert, issue: issue, project: project) }
-
it 'resolves an alert and sends a system note' do
- expect_any_instance_of(SystemNoteService) do |notes_service|
- expect(notes_service).to receive(:change_alert_status).with(
- alert,
- current_user,
- " by closing issue #{issue.to_reference(project)}"
- )
- end
+ alert = create(:alert_management_alert, issue: issue, project: project)
+
+ expect(SystemNoteService).to receive(:change_alert_status)
+ .with(alert, User.alert_bot, " because #{user.to_reference} closed incident #{issue.to_reference(project)}")
close_issue
- expect(alert.reload.resolved?).to eq(true)
+ expect(alert.reload).to be_resolved
end
end
context 'when alert cannot be resolved' do
- let!(:alert) { create(:alert_management_alert, :with_validation_errors, issue: issue, project: project) }
-
before do
allow(Gitlab::AppLogger).to receive(:warn).and_call_original
end
it 'writes a warning into the log' do
+ alert = create(:alert_management_alert, :with_validation_errors, issue: issue, project: project)
+
close_issue
expect(Gitlab::AppLogger).to have_received(:warn).with(
@@ -383,6 +386,23 @@ RSpec.describe Issues::CloseService do
end
end
+ context 'when there are several associated Alert Management Alerts' do
+ context 'when alerts can be resolved' do
+ it 'resolves an alert and sends a system note', :aggregate_failures do
+ alerts = create_list(:alert_management_alert, 2, issue: issue, project: project)
+
+ alerts.each do |alert|
+ expect(SystemNoteService).to receive(:change_alert_status)
+ .with(alert, User.alert_bot, " because #{user.to_reference} closed incident #{issue.to_reference(project)}")
+ end
+
+ close_issue
+
+ expect(alerts.map(&:reload)).to all(be_resolved)
+ end
+ end
+ end
+
it 'deletes milestone issue counters cache' do
issue.update!(milestone: create(:milestone, project: project))
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 5ddf91e167e..7ab2046b6be 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -9,21 +9,22 @@ RSpec.describe Issues::CreateService do
let_it_be_with_reload(:project) { create(:project, :public, group: group) }
let_it_be(:user) { create(:user) }
+ let(:opts) { { title: 'title' } }
let(:spam_params) { double }
+ let(:service) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params) }
it_behaves_like 'rate limited service' do
let(:key) { :issues_create }
let(:key_scope) { %i[project current_user external_author] }
let(:application_limit_key) { :issues_create_limit }
let(:created_model) { Issue }
- let(:service) { described_class.new(project: project, current_user: user, params: { title: 'title' }, spam_params: double) }
end
describe '#execute' do
let_it_be(:assignee) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
- let(:result) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute }
+ let(:result) { service.execute }
let(:issue) { result[:issue] }
before do
@@ -54,6 +55,7 @@ RSpec.describe Issues::CreateService do
let(:opts) do
{ title: 'Awesome issue',
+ issue_type: :task,
description: 'please fix',
assignee_ids: [assignee.id],
label_ids: labels.map(&:id),
@@ -118,10 +120,26 @@ RSpec.describe Issues::CreateService do
expect(issue.labels).to match_array(labels)
expect(issue.milestone).to eq(milestone)
expect(issue.due_date).to eq(Date.tomorrow)
- expect(issue.work_item_type.base_type).to eq('issue')
+ expect(issue.work_item_type.base_type).to eq('task')
expect(issue.issue_customer_relations_contacts).to be_empty
end
+ context 'when the work item type is not allowed to create' do
+ before do
+ allow_next_instance_of(::Issues::BuildService) do |instance|
+ allow(instance).to receive(:create_issue_type_allowed?).twice.and_return(false)
+ end
+ end
+
+ it 'ignores the type and creates default issue' do
+ expect(result).to be_success
+ expect(issue).to be_persisted
+ expect(issue).to be_a(::Issue)
+ expect(issue.work_item_type.base_type).to eq('issue')
+ expect(issue.issue_type).to eq('issue')
+ end
+ end
+
it 'calls NewIssueWorker with correct arguments' do
expect(NewIssueWorker).to receive(:perform_async).with(Integer, user.id, 'Issue')
@@ -405,7 +423,8 @@ RSpec.describe Issues::CreateService do
iid: { current: kind_of(Integer), previous: nil },
project_id: { current: project.id, previous: nil },
title: { current: opts[:title], previous: nil },
- updated_at: { current: kind_of(Time), previous: nil }
+ updated_at: { current: kind_of(Time), previous: nil },
+ time_estimate: { current: 0, previous: nil }
},
object_attributes: include(
opts.merge(
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 655c5085fdc..324b2aa9fe2 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -508,4 +508,25 @@ RSpec.describe Issues::MoveService do
end
end
end
+
+ context 'copying email participants' do
+ let!(:participant1) { create(:issue_email_participant, email: 'user1@example.com', issue: old_issue) }
+ let!(:participant2) { create(:issue_email_participant, email: 'user2@example.com', issue: old_issue) }
+ let!(:participant3) { create(:issue_email_participant, email: 'other_project_customer@example.com') }
+
+ include_context 'user can move issue'
+
+ subject(:new_issue) do
+ move_service.execute(old_issue, new_project)
+ end
+
+ it 'copies moved issue email participants' do
+ new_issue
+
+ expect(participant1.reload.issue).to eq(old_issue)
+ expect(participant2.reload.issue).to eq(old_issue)
+ expect(new_issue.issue_email_participants.pluck(:email))
+ .to match_array([participant1.email, participant2.email])
+ end
+ end
end
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 6013826f9b1..529b3ff266b 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -74,6 +74,14 @@ RSpec.describe Issues::ReopenService do
it_behaves_like 'an incident management tracked event', :incident_management_incident_reopened
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { issue.namespace }
+ let(:category) { described_class.to_s }
+ let(:action) { 'incident_management_incident_reopened' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
+
it 'creates a timeline event' do
expect(IncidentManagement::TimelineEvents::CreateService)
.to receive(:reopen_incident)
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index f1ee62fd589..70fc6ffc38f 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Issues::UpdateService, :mailer do
description: 'Also please fix',
assignee_ids: [user2.id],
state_event: 'close',
- label_ids: [label.id],
+ label_ids: [label&.id],
due_date: Date.tomorrow,
discussion_locked: true,
severity: 'low',
@@ -189,6 +189,27 @@ RSpec.describe Issues::UpdateService, :mailer do
subject { update_issue(confidential: true) }
it_behaves_like 'an incident management tracked event', :incident_management_incident_change_confidential
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { issue.namespace }
+ let(:category) { described_class.to_s }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ let(:action) { 'incident_management_incident_change_confidential' }
+ let(:opts) do
+ {
+ title: 'New title',
+ description: 'Also please fix',
+ assignee_ids: [user2.id],
+ state_event: 'close',
+ due_date: Date.tomorrow,
+ discussion_locked: true,
+ severity: 'low',
+ milestone_id: milestone.id,
+ add_contacts: [contact.email]
+ }
+ end
+ end
end
end
@@ -673,6 +694,14 @@ RSpec.describe Issues::UpdateService, :mailer do
let(:current_user) { user }
it_behaves_like 'an incident management tracked event', :incident_management_incident_assigned
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { issue.namespace }
+ let(:category) { described_class.to_s }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ let(:action) { "incident_management_incident_assigned" }
+ end
end
end
diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb
index d662d9fa978..ad1f91ab5e6 100644
--- a/spec/services/issues/zoom_link_service_spec.rb
+++ b/spec/services/issues/zoom_link_service_spec.rb
@@ -95,6 +95,14 @@ RSpec.describe Issues::ZoomLinkService do
let(:current_user) { user }
it_behaves_like 'an incident management tracked event', :incident_management_incident_zoom_meeting
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { issue.namespace }
+ let(:category) { described_class.to_s }
+ let(:action) { 'incident_management_incident_zoom_meeting' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
end
context 'with insufficient issue update permissions' do
diff --git a/spec/services/jira_connect/create_asymmetric_jwt_service_spec.rb b/spec/services/jira_connect/create_asymmetric_jwt_service_spec.rb
index f5359e5b643..bb96e327307 100644
--- a/spec/services/jira_connect/create_asymmetric_jwt_service_spec.rb
+++ b/spec/services/jira_connect/create_asymmetric_jwt_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnect::CreateAsymmetricJwtService do
+RSpec.describe JiraConnect::CreateAsymmetricJwtService, feature_category: :integrations do
describe '#execute' do
let_it_be(:jira_connect_installation) { create(:jira_connect_installation) }
@@ -19,27 +19,40 @@ RSpec.describe JiraConnect::CreateAsymmetricJwtService do
let(:public_key_id) { Atlassian::Jwt.decode(jwt_token, nil, false, algorithm: 'RS256').last['kid'] }
let(:public_key_cdn) { 'https://gitlab.com/-/jira_connect/public_keys/' }
+ let(:event_url) { 'https://gitlab.test/-/jira_connect/events/installed' }
let(:jwt_verification_claims) do
{
aud: 'https://gitlab.test/-/jira_connect',
iss: jira_connect_installation.client_key,
- qsh: Atlassian::Jwt.create_query_string_hash('https://gitlab.test/-/jira_connect/events/installed', 'POST', 'https://gitlab.test/-/jira_connect')
+ qsh: Atlassian::Jwt.create_query_string_hash(event_url, 'POST', 'https://gitlab.test/-/jira_connect')
}
end
subject(:jwt_token) { service.execute }
+ shared_examples 'produces a valid JWT' do
+ it 'produces a valid JWT' do
+ public_key = OpenSSL::PKey.read(JiraConnect::PublicKey.find(public_key_id).key)
+ options = jwt_verification_claims.except(:qsh).merge({ verify_aud: true, verify_iss: true,
+ algorithm: 'RS256' })
+
+ decoded_token = Atlassian::Jwt.decode(jwt_token, public_key, true, options).first
+
+ expect(decoded_token).to eq(jwt_verification_claims.stringify_keys)
+ end
+ end
+
it 'stores the public key' do
expect { JiraConnect::PublicKey.find(public_key_id) }.not_to raise_error
end
- it 'is produces a valid JWT' do
- public_key = OpenSSL::PKey.read(JiraConnect::PublicKey.find(public_key_id).key)
- options = jwt_verification_claims.except(:qsh).merge({ verify_aud: true, verify_iss: true, algorithm: 'RS256' })
+ it_behaves_like 'produces a valid JWT'
- decoded_token = Atlassian::Jwt.decode(jwt_token, public_key, true, options).first
+ context 'with uninstalled event option' do
+ let(:service) { described_class.new(jira_connect_installation, event: :uninstalled) }
+ let(:event_url) { 'https://gitlab.test/-/jira_connect/events/uninstalled' }
- expect(decoded_token).to eq(jwt_verification_claims.stringify_keys)
+ it_behaves_like 'produces a valid JWT'
end
end
end
diff --git a/spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb b/spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb
new file mode 100644
index 00000000000..c621388a734
--- /dev/null
+++ b/spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe JiraConnectInstallations::ProxyLifecycleEventService, feature_category: :integrations do
+ describe '.execute' do
+ let(:installation) { create(:jira_connect_installation) }
+
+ it 'creates an instance and calls execute' do
+ expect_next_instance_of(described_class, installation, 'installed', 'https://test.gitlab.com') do |update_service|
+ expect(update_service).to receive(:execute)
+ end
+
+ described_class.execute(installation, 'installed', 'https://test.gitlab.com')
+ end
+ end
+
+ describe '.new' do
+ let_it_be(:installation) { create(:jira_connect_installation, instance_url: nil) }
+
+ let(:event) { :installed }
+
+ subject(:service) { described_class.new(installation, event, 'https://test.gitlab.com') }
+
+ it 'creates an internal duplicate of the installation and sets the instance_url' do
+ expect(service.instance_variable_get(:@installation).instance_url).to eq('https://test.gitlab.com')
+ end
+
+ context 'with unknown event' do
+ let(:event) { 'test' }
+
+ it 'raises an error' do
+ expect { service }.to raise_error(ArgumentError, 'Unknown event \'test\'')
+ end
+ end
+ end
+
+ describe '#execute' do
+ let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://old_instance_url.example.com') }
+
+ let(:service) { described_class.new(installation, evnet_type, 'https://gitlab.example.com') }
+ let(:service_instance_installation) { service.instance_variable_get(:@installation) }
+
+ before do
+ allow_next_instance_of(JiraConnect::CreateAsymmetricJwtService) do |create_asymmetric_jwt_service|
+ allow(create_asymmetric_jwt_service).to receive(:execute).and_return('123456')
+ end
+
+ stub_request(:post, hook_url)
+ end
+
+ subject(:execute_service) { service.execute }
+
+ shared_examples 'sends the event hook' do
+ it 'returns a ServiceResponse' do
+ expect(execute_service).to be_kind_of(ServiceResponse)
+ expect(execute_service[:status]).to eq(:success)
+ end
+
+ it 'sends an installed event to the instance' do
+ execute_service
+
+ expect(WebMock).to have_requested(:post, hook_url).with(body: expected_request_body)
+ end
+
+ it 'creates the JWT token with the event and installation' do
+ expect_next_instance_of(
+ JiraConnect::CreateAsymmetricJwtService,
+ service_instance_installation,
+ event: evnet_type
+ ) do |create_asymmetric_jwt_service|
+ expect(create_asymmetric_jwt_service).to receive(:execute).and_return('123456')
+ end
+
+ expect(execute_service[:status]).to eq(:success)
+ end
+
+ context 'and the instance responds with an error' do
+ before do
+ stub_request(:post, hook_url).to_return(
+ status: 422,
+ body: 'Error message',
+ headers: {}
+ )
+ end
+
+ it 'returns an error ServiceResponse', :aggregate_failures do
+ expect(execute_service).to be_kind_of(ServiceResponse)
+ expect(execute_service[:status]).to eq(:error)
+ expect(execute_service[:message]).to eq( { type: :response_error, code: 422 } )
+ end
+
+ it 'logs the error response' do
+ expect(Gitlab::IntegrationsLogger).to receive(:info).with(
+ integration: 'JiraConnect',
+ message: 'Proxy lifecycle event received error response',
+ event_type: evnet_type,
+ status_code: 422,
+ body: 'Error message'
+ )
+
+ execute_service
+ end
+ end
+
+ context 'and the request raises an error' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED, 'error message')
+ end
+
+ it 'returns an error ServiceResponse', :aggregate_failures do
+ expect(execute_service).to be_kind_of(ServiceResponse)
+ expect(execute_service[:status]).to eq(:error)
+ expect(execute_service[:message]).to eq(
+ {
+ type: :network_error,
+ message: 'Connection refused - error message'
+ }
+ )
+ end
+ end
+ end
+
+ context 'when installed event' do
+ let(:evnet_type) { :installed }
+ let(:hook_url) { 'https://gitlab.example.com/-/jira_connect/events/installed' }
+ let(:expected_request_body) do
+ {
+ clientKey: installation.client_key,
+ sharedSecret: installation.shared_secret,
+ baseUrl: installation.base_url,
+ jwt: '123456',
+ eventType: 'installed'
+ }
+ end
+
+ it_behaves_like 'sends the event hook'
+ end
+
+ context 'when uninstalled event' do
+ let(:evnet_type) { :uninstalled }
+ let(:hook_url) { 'https://gitlab.example.com/-/jira_connect/events/uninstalled' }
+ let(:expected_request_body) do
+ {
+ clientKey: installation.client_key,
+ jwt: '123456',
+ eventType: 'uninstalled'
+ }
+ end
+
+ it_behaves_like 'sends the event hook'
+ end
+ end
+end
diff --git a/spec/services/jira_connect_installations/update_service_spec.rb b/spec/services/jira_connect_installations/update_service_spec.rb
new file mode 100644
index 00000000000..ec5bb5d6d6a
--- /dev/null
+++ b/spec/services/jira_connect_installations/update_service_spec.rb
@@ -0,0 +1,186 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe JiraConnectInstallations::UpdateService, feature_category: :integrations do
+ describe '.execute' do
+ it 'creates an instance and calls execute' do
+ expect_next_instance_of(described_class, 'param1', 'param2') do |update_service|
+ expect(update_service).to receive(:execute)
+ end
+
+ described_class.execute('param1', 'param2')
+ end
+ end
+
+ describe '#execute' do
+ let_it_be_with_reload(:installation) { create(:jira_connect_installation) }
+ let(:update_params) { { client_key: 'new_client_key' } }
+
+ subject(:execute_service) { described_class.new(installation, update_params).execute }
+
+ it 'returns a ServiceResponse' do
+ expect(execute_service).to be_kind_of(ServiceResponse)
+ expect(execute_service[:status]).to eq(:success)
+ end
+
+ it 'updates the installation' do
+ expect { execute_service }.to change { installation.client_key }.to('new_client_key')
+ end
+
+ it 'returns a successful result' do
+ expect(execute_service.success?).to eq(true)
+ end
+
+ context 'and model validation fails' do
+ let(:update_params) { { instance_url: 'invalid' } }
+
+ it 'returns an error result' do
+ expect(execute_service.error?).to eq(true)
+ expect(execute_service.message).to eq(installation.errors)
+ end
+ end
+
+ context 'and the installation has an instance_url' do
+ let_it_be_with_reload(:installation) { create(:jira_connect_installation, instance_url: 'https://other_gitlab.example.com') }
+
+ it 'sends an installed event to the instance', :aggregate_failures do
+ expect_next_instance_of(JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed,
+'https://other_gitlab.example.com') do |proxy_lifecycle_events_service|
+ expect(proxy_lifecycle_events_service).to receive(:execute).and_return(ServiceResponse.new(status: :success))
+ end
+
+ expect(JiraConnect::SendUninstalledHookWorker).not_to receive(:perform_async)
+
+ expect { execute_service }.not_to change { installation.instance_url }
+ end
+
+ context 'and instance_url gets updated' do
+ let(:update_params) { { instance_url: 'https://gitlab.example.com' } }
+
+ before do
+ stub_request(:post, 'https://other_gitlab.example.com/-/jira_connect/events/uninstalled')
+ end
+
+ it 'starts an async worker to send an uninstalled event to the previous instance' do
+ expect(JiraConnect::SendUninstalledHookWorker).to receive(:perform_async).with(installation.id, 'https://other_gitlab.example.com')
+
+ expect(JiraConnectInstallations::ProxyLifecycleEventService)
+ .to receive(:execute).with(installation, :installed, 'https://gitlab.example.com')
+ .and_return(ServiceResponse.new(status: :success))
+
+ execute_service
+
+ expect(installation.instance_url).to eq(update_params[:instance_url])
+ end
+
+ context 'and the new instance_url is empty' do
+ let(:update_params) { { instance_url: nil } }
+
+ it 'starts an async worker to send an uninstalled event to the previous instance' do
+ expect(JiraConnect::SendUninstalledHookWorker).to receive(:perform_async).with(installation.id, 'https://other_gitlab.example.com')
+
+ execute_service
+
+ expect(installation.instance_url).to eq(nil)
+ end
+
+ it 'does not send an installed event' do
+ expect(JiraConnectInstallations::ProxyLifecycleEventService).not_to receive(:new)
+
+ execute_service
+ end
+ end
+ end
+ end
+
+ context 'and instance_url is updated' do
+ let(:update_params) { { instance_url: 'https://gitlab.example.com' } }
+
+ it 'sends an installed event to the instance and updates instance_url' do
+ expect_next_instance_of(JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed,
+'https://gitlab.example.com') do |proxy_lifecycle_events_service|
+ expect(proxy_lifecycle_events_service).to receive(:execute).and_return(ServiceResponse.new(status: :success))
+ end
+
+ expect(JiraConnect::SendUninstalledHookWorker).not_to receive(:perform_async)
+
+ execute_service
+
+ expect(installation.instance_url).to eq(update_params[:instance_url])
+ end
+
+ context 'and the instance installation cannot be created' do
+ before do
+ allow_next_instance_of(
+ JiraConnectInstallations::ProxyLifecycleEventService,
+ installation,
+ :installed,
+ 'https://gitlab.example.com'
+ ) do |proxy_lifecycle_events_service|
+ allow(proxy_lifecycle_events_service).to receive(:execute).and_return(
+ ServiceResponse.error(
+ message: {
+ type: :response_error,
+ code: '422'
+ }
+ )
+ )
+ end
+ end
+
+ it 'does not change instance_url' do
+ expect { execute_service }.not_to change { installation.instance_url }
+ end
+
+ it 'returns an error message' do
+ expect(execute_service[:status]).to eq(:error)
+ expect(execute_service[:message]).to eq(
+ {
+ instance_url: ["Could not be installed on the instance. Error response code 422"]
+ }
+ )
+ end
+
+ context 'and the installation had a previous instance_url' do
+ let(:installation) { build(:jira_connect_installation, instance_url: 'https://other_gitlab.example.com') }
+
+ it 'does not send the uninstalled hook to the previous instance_url' do
+ expect(JiraConnect::SendUninstalledHookWorker).not_to receive(:perform_async)
+
+ execute_service
+ end
+ end
+
+ context 'when failure because of a network error' do
+ before do
+ allow_next_instance_of(
+ JiraConnectInstallations::ProxyLifecycleEventService,
+ installation,
+ :installed,
+ 'https://gitlab.example.com'
+ ) do |proxy_lifecycle_events_service|
+ allow(proxy_lifecycle_events_service).to receive(:execute).and_return(
+ ServiceResponse.error(
+ message: {
+ type: :network_error,
+ message: 'Connection refused - error message'
+ }
+ )
+ )
+ end
+ end
+
+ it 'returns an error message' do
+ expect(execute_service[:status]).to eq(:error)
+ expect(execute_service[:message]).to eq(
+ {
+ instance_url: ["Could not be installed on the instance. Network error"]
+ }
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/markup/rendering_service_spec.rb b/spec/services/markup/rendering_service_spec.rb
index d54bc71f0a4..99ab87f2072 100644
--- a/spec/services/markup/rendering_service_spec.rb
+++ b/spec/services/markup/rendering_service_spec.rb
@@ -75,25 +75,6 @@ RSpec.describe Markup::RenderingService do
is_expected.to eq(expected_html)
end
-
- context 'when renderer returns an error' do
- before do
- allow(Banzai).to receive(:render).and_raise(StandardError, "An error")
- end
-
- it 'returns html (rendered by ActionView:TextHelper)' do
- is_expected.to eq('<p>Noël</p>')
- end
-
- it 'logs the error' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- instance_of(StandardError),
- project_id: context[:project].id, file_name: 'foo.md'
- )
-
- subject
- end
- end
end
context 'when file is asciidoc file' do
@@ -130,37 +111,5 @@ RSpec.describe Markup::RenderingService do
is_expected.to eq(expected_html)
end
end
-
- context 'when rendering takes too long' do
- let(:file_name) { 'foo.bar' }
-
- before do
- stub_const("Markup::RenderingService::RENDER_TIMEOUT", 0.1)
- allow(Gitlab::OtherMarkup).to receive(:render) do
- sleep(0.2)
- text
- end
- end
-
- it 'times out' do
- expect(Gitlab::RenderTimeout).to receive(:timeout).and_call_original
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- instance_of(Timeout::Error),
- project_id: context[:project].id, file_name: file_name
- )
-
- is_expected.to eq("<p>#{text}</p>")
- end
-
- context 'when markup_rendering_timeout is disabled' do
- it 'waits until the execution completes' do
- stub_feature_flags(markup_rendering_timeout: false)
-
- expect(Gitlab::RenderTimeout).not_to receive(:timeout)
-
- is_expected.to eq(text)
- end
- end
- end
end
end
diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb
index 2155b4ffad1..f477b2166d9 100644
--- a/spec/services/merge_requests/after_create_service_spec.rb
+++ b/spec/services/merge_requests/after_create_service_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe MergeRequests::AfterCreateService do
it 'calls the merge request activity counter' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_create_mr_action)
- .with(user: merge_request.author)
+ .with(user: merge_request.author, merge_request: merge_request)
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_mr_including_ci_config)
diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb
index da6492aca95..1d6427900b9 100644
--- a/spec/services/merge_requests/approval_service_spec.rb
+++ b/spec/services/merge_requests/approval_service_spec.rb
@@ -91,6 +91,10 @@ RSpec.describe MergeRequests::ApprovalService do
it_behaves_like 'triggers GraphQL subscription mergeRequestReviewersUpdated' do
let(:action) { service.execute(merge_request) }
end
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestApprovalStateUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
end
context 'user cannot update the merge request' do
@@ -109,6 +113,10 @@ RSpec.describe MergeRequests::ApprovalService do
it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
let(:action) { service.execute(merge_request) }
end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestApprovalStateUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
end
end
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 4f27ff30da7..79c779678a4 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -25,7 +25,9 @@ RSpec.describe MergeRequests::BuildService do
safe_message: 'Initial commit',
gitaly_commit?: false,
id: 'f00ba6',
- parent_ids: ['f00ba5'])
+ parent_ids: ['f00ba5'],
+ author_email: 'tom@example.com',
+ author_name: 'Tom Example')
end
let(:commit_2) do
@@ -34,7 +36,9 @@ RSpec.describe MergeRequests::BuildService do
safe_message: "Closes #1234 Second commit\n\nCreate the app",
gitaly_commit?: false,
id: 'f00ba7',
- parent_ids: ['f00ba6'])
+ parent_ids: ['f00ba6'],
+ author_email: 'alice@example.com',
+ author_name: 'Alice Example')
end
let(:commit_3) do
@@ -43,7 +47,9 @@ RSpec.describe MergeRequests::BuildService do
safe_message: 'This is a bad commit message!',
gitaly_commit?: false,
id: 'f00ba8',
- parent_ids: ['f00ba7'])
+ parent_ids: ['f00ba7'],
+ author_email: 'jo@example.com',
+ author_name: 'Jo Example')
end
let(:commits) { nil }
@@ -742,4 +748,91 @@ RSpec.describe MergeRequests::BuildService do
end
end
end
+
+ describe '#replace_variables_in_description' do
+ context 'when the merge request description is blank' do
+ let(:description) { nil }
+
+ it 'does not update the description' do
+ expect(merge_request.description).to eq(nil)
+ end
+ end
+
+ context 'when the merge request description contains template variables' do
+ let(:description) { <<~MSG.rstrip }
+ source_branch:%{source_branch}
+ target_branch:%{target_branch}
+ title:%{title}
+ issues:%{issues}
+ description:%{description}
+ first_commit:%{first_commit}
+ first_multiline_commit:%{first_multiline_commit}
+ url:%{url}
+ approved_by:%{approved_by}
+ merged_by:%{merged_by}
+ co_authored_by:%{co_authored_by}
+ all_commits:%{all_commits}
+ MSG
+
+ context 'when there are multiple commits in the diff' do
+ let(:commits) { Commit.decorate([commit_1, commit_2, commit_3], project) }
+
+ before do
+ stub_compare
+ end
+
+ it 'replaces the variables in the description' do
+ expect(merge_request.description).to eq <<~MSG.rstrip
+ source_branch:feature-branch
+ target_branch:master
+ title:
+ issues:
+ description:
+ first_commit:Initial commit
+ first_multiline_commit:Closes #1234 Second commit
+
+ Create the app
+ url:
+ approved_by:
+ merged_by:
+ co_authored_by:Co-authored-by: Jo Example <jo@example.com>
+ Co-authored-by: Alice Example <alice@example.com>
+ Co-authored-by: Tom Example <tom@example.com>
+ all_commits:* This is a bad commit message!
+
+ * Closes #1234 Second commit
+
+ Create the app
+
+ * Initial commit
+ MSG
+ end
+ end
+
+ context 'when there are no commits in the diff' do
+ let(:commits) { [] }
+
+ before do
+ stub_compare
+ end
+
+ it 'replaces the variables in the description' do
+ expect(merge_request.description).to eq <<~MSG.rstrip
+ source_branch:feature-branch
+ target_branch:master
+ title:
+ issues:
+ description:
+ first_commit:
+ first_multiline_commit:
+ url:
+ approved_by:
+ merged_by:
+ co_authored_by:
+ all_commits:
+ MSG
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb
index dc96b5c0e5e..7984fff3031 100644
--- a/spec/services/merge_requests/create_pipeline_service_spec.rb
+++ b/spec/services/merge_requests/create_pipeline_service_spec.rb
@@ -223,5 +223,26 @@ RSpec.describe MergeRequests::CreatePipelineService, :clean_gitlab_redis_cache d
expect(response.payload).to be_nil
end
end
+
+ context 'when merge request pipeline creates a dynamic environment' do
+ let(:config) do
+ {
+ review_app: {
+ script: 'echo',
+ only: ['merge_requests'],
+ environment: { name: "review/$CI_COMMIT_REF_NAME" }
+ }
+ }
+ end
+
+ it 'associates merge request with the environment' do
+ expect { response }.to change { Ci::Pipeline.count }.by(1)
+
+ environment = Environment.find_by_name('review/feature')
+ expect(response).to be_success
+ expect(environment).to be_present
+ expect(environment.merge_request).to eq(merge_request)
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/remove_approval_service_spec.rb b/spec/services/merge_requests/remove_approval_service_spec.rb
index 7b38f0d1c45..fd8240935e8 100644
--- a/spec/services/merge_requests/remove_approval_service_spec.rb
+++ b/spec/services/merge_requests/remove_approval_service_spec.rb
@@ -53,6 +53,10 @@ RSpec.describe MergeRequests::RemoveApprovalService do
it_behaves_like 'triggers GraphQL subscription mergeRequestReviewersUpdated' do
let(:action) { execute! }
end
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestApprovalStateUpdated' do
+ let(:action) { execute! }
+ end
end
context 'with a user who has not approved' do
@@ -77,6 +81,10 @@ RSpec.describe MergeRequests::RemoveApprovalService do
it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
let(:action) { execute! }
end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestApprovalStateUpdated' do
+ let(:action) { execute! }
+ end
end
end
end
diff --git a/spec/services/ml/experiment_tracking/candidate_repository_spec.rb b/spec/services/ml/experiment_tracking/candidate_repository_spec.rb
index 8002b2ebc86..ff3b295d185 100644
--- a/spec/services/ml/experiment_tracking/candidate_repository_spec.rb
+++ b/spec/services/ml/experiment_tracking/candidate_repository_spec.rb
@@ -31,13 +31,17 @@ RSpec.describe ::Ml::ExperimentTracking::CandidateRepository do
end
describe '#create!' do
- subject { repository.create!(experiment, 1234) }
+ subject { repository.create!(experiment, 1234, [{ key: 'hello', value: 'world' }]) }
it 'creates the candidate' do
expect(subject.start_time).to eq(1234)
expect(subject.iid).not_to be_nil
expect(subject.end_time).to be_nil
end
+
+ it 'creates with tag' do
+ expect(subject.metadata.length).to eq(1)
+ end
end
describe '#update' do
@@ -118,6 +122,32 @@ RSpec.describe ::Ml::ExperimentTracking::CandidateRepository do
end
end
+ describe '#add_tag!' do
+ let(:props) { { name: 'abc', value: 'def' } }
+
+ subject { repository.add_tag!(candidate, props[:name], props[:value]) }
+
+ it 'adds a new tag' do
+ expect { subject }.to change { candidate.reload.metadata.size }.by(1)
+ end
+
+ context 'when name missing' do
+ let(:props) { { value: 1234 } }
+
+ it 'throws RecordInvalid' do
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+
+ context 'when tag was already added' do
+ it 'throws RecordInvalid' do
+ repository.add_tag!(candidate, 'new', props[:value])
+
+ expect { repository.add_tag!(candidate, 'new', props[:value]) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+
describe "#add_params" do
let(:params) do
[{ key: 'model_class', value: 'LogisticRegression' }, { 'key': 'pythonEnv', value: '3.10' }]
@@ -196,4 +226,50 @@ RSpec.describe ::Ml::ExperimentTracking::CandidateRepository do
end
end
end
+
+ describe "#add_tags" do
+ let(:tags) do
+ [{ key: 'gitlab.tag1', value: 'hello' }, { 'key': 'gitlab.tag2', value: 'world' }]
+ end
+
+ subject { repository.add_tags(candidate, tags) }
+
+ it 'adds the tags' do
+ expect { subject }.to change { candidate.reload.metadata.size }.by(2)
+ end
+
+ context 'if tags misses key' do
+ let(:tags) { [{ value: 'hello' }] }
+
+ it 'does throw and does not add' do
+ expect { subject }.to raise_error(ActiveRecord::ActiveRecordError)
+ end
+ end
+
+ context 'if tag misses value' do
+ let(:tags) { [{ key: 'gitlab.tag1' }] }
+
+ it 'does throw and does not add' do
+ expect { subject }.to raise_error(ActiveRecord::ActiveRecordError)
+ end
+ end
+
+ context 'if tag repeated' do
+ let(:params) do
+ [
+ { 'key': 'gitlab.tag1', value: 'hello' },
+ { 'key': 'gitlab.tag2', value: 'world' },
+ { 'key': 'gitlab.tag1', value: 'gitlab' }
+ ]
+ end
+
+ before do
+ repository.add_tag!(candidate, 'gitlab.tag2', '0')
+ end
+
+ it 'does not throw and adds only the first of each kind' do
+ expect { subject }.to change { candidate.reload.metadata.size }.by(1)
+ end
+ end
+ end
end
diff --git a/spec/services/ml/experiment_tracking/experiment_repository_spec.rb b/spec/services/ml/experiment_tracking/experiment_repository_spec.rb
index 80e1fa025d1..c3c716b831a 100644
--- a/spec/services/ml/experiment_tracking/experiment_repository_spec.rb
+++ b/spec/services/ml/experiment_tracking/experiment_repository_spec.rb
@@ -59,10 +59,11 @@ RSpec.describe ::Ml::ExperimentTracking::ExperimentRepository do
describe '#create!' do
let(:name) { 'hello' }
+ let(:tags) { nil }
- subject { repository.create!(name) }
+ subject { repository.create!(name, tags) }
- it 'creates the candidate' do
+ it 'creates the experiment' do
expect { subject }.to change { repository.all.size }.by(1)
end
@@ -74,6 +75,14 @@ RSpec.describe ::Ml::ExperimentTracking::ExperimentRepository do
end
end
+ context 'when has tags' do
+ let(:tags) { [{ key: 'hello', value: 'world' }] }
+
+ it 'creates the experiment with tag' do
+ expect(subject.metadata.length).to eq(1)
+ end
+ end
+
context 'when name is missing' do
let(:name) { nil }
@@ -82,4 +91,30 @@ RSpec.describe ::Ml::ExperimentTracking::ExperimentRepository do
end
end
end
+
+ describe '#add_tag!' do
+ let(:props) { { name: 'abc', value: 'def' } }
+
+ subject { repository.add_tag!(experiment, props[:name], props[:value]) }
+
+ it 'adds a new tag' do
+ expect { subject }.to change { experiment.reload.metadata.size }.by(1)
+ end
+
+ context 'when name missing' do
+ let(:props) { { value: 1234 } }
+
+ it 'throws RecordInvalid' do
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+
+ context 'when tag was already added' do
+ it 'throws RecordInvalid' do
+ repository.add_tag!(experiment, 'new', props[:value])
+
+ expect { repository.add_tag!(experiment, 'new', props[:value]) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 4922e72b7a4..2f1c5a5b0f3 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -102,6 +102,14 @@ RSpec.describe Notes::CreateService do
it_behaves_like 'an incident management tracked event', :incident_management_incident_comment do
let(:current_user) { user }
end
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { issue.namespace }
+ let(:category) { described_class.to_s }
+ let(:action) { 'incident_management_incident_comment' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ end
end
describe 'event tracking', :snowplow do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 7857bd2263f..1ca14cd430b 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -361,8 +361,14 @@ RSpec.describe NotificationService, :mailer do
subject(:notification_service) { notification.access_token_revoked(user, pat.name) }
- it 'sends email to the token owner' do
- expect { notification_service }.to have_enqueued_email(user, pat.name, mail: "access_token_revoked_email")
+ it 'sends email to the token owner without source' do
+ expect { notification_service }.to have_enqueued_email(user, pat.name, nil, mail: "access_token_revoked_email")
+ end
+
+ it 'sends email to the token owner with source' do
+ expect do
+ notification.access_token_revoked(user, pat.name, 'secret_detection')
+ end.to have_enqueued_email(user, pat.name, 'secret_detection', mail: "access_token_revoked_email")
end
context 'when user is not allowed to receive notifications' do
diff --git a/spec/services/packages/debian/process_changes_service_spec.rb b/spec/services/packages/debian/process_changes_service_spec.rb
index a45dd68cd6e..27b49a13d52 100644
--- a/spec/services/packages/debian/process_changes_service_spec.rb
+++ b/spec/services/packages/debian/process_changes_service_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Packages::Debian::ProcessChangesService do
end
context 'marked as pending_destruction' do
- it 'creates a package' do
+ it 'does not re-use the existing package' do
existing_package.pending_destruction!
expect { subject.execute }
@@ -73,7 +73,7 @@ RSpec.describe Packages::Debian::ProcessChangesService do
end
end
- it 'remove the package file', :aggregate_failures do
+ it 're-raise error', :aggregate_failures do
expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
expect { subject.execute }
.to not_change { Packages::Package.count }
diff --git a/spec/services/packages/debian/process_package_file_service_spec.rb b/spec/services/packages/debian/process_package_file_service_spec.rb
new file mode 100644
index 00000000000..571861f42cf
--- /dev/null
+++ b/spec/services/packages/debian/process_package_file_service_spec.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::ProcessPackageFileService do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, codename: 'unstable') }
+
+ let!(:incoming) { create(:debian_incoming, project: distribution.project) }
+
+ let(:distribution_name) { distribution.codename }
+ let(:debian_file_metadatum) { package_file.debian_file_metadatum }
+
+ subject { described_class.new(package_file, user, distribution_name, component_name) }
+
+ RSpec.shared_context 'with Debian package file' do |file_name|
+ let(:package_file) { incoming.package_files.with_file_name(file_name).first }
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:case_name, :expected_file_type, :file_name, :component_name) do
+ 'with a deb' | 'deb' | 'libsample0_1.2.3~alpha2_amd64.deb' | 'main'
+ 'with an udeb' | 'udeb' | 'sample-udeb_1.2.3~alpha2_amd64.udeb' | 'contrib'
+ end
+
+ with_them do
+ include_context 'with Debian package file', params[:file_name] do
+ it 'creates package and updates package file', :aggregate_failures do
+ expect(::Packages::Debian::GenerateDistributionWorker)
+ .to receive(:perform_async).with(:project, distribution.id)
+ expect { subject.execute }
+ .to change(Packages::Package, :count).from(1).to(2)
+ .and not_change(Packages::PackageFile, :count)
+ .and change(incoming.package_files, :count).from(7).to(6)
+ .and change(debian_file_metadatum, :file_type).from('unknown').to(expected_file_type)
+ .and change(debian_file_metadatum, :component).from(nil).to(component_name)
+
+ created_package = Packages::Package.last
+ expect(created_package.name).to eq 'sample'
+ expect(created_package.version).to eq '1.2.3~alpha2'
+ expect(created_package.creator).to eq user
+ end
+
+ context 'with existing package' do
+ let_it_be_with_reload(:existing_package) do
+ create(:debian_package, name: 'sample', version: '1.2.3~alpha2', project: distribution.project)
+ end
+
+ before do
+ existing_package.update!(debian_distribution: distribution)
+ end
+
+ it 'does not create a package and assigns the package_file to the existing package' do
+ expect(::Packages::Debian::GenerateDistributionWorker)
+ .to receive(:perform_async).with(:project, distribution.id)
+ expect { subject.execute }
+ .to not_change(Packages::Package, :count)
+ .and not_change(Packages::PackageFile, :count)
+ .and change(incoming.package_files, :count).from(7).to(6)
+ .and change(package_file, :package).from(incoming).to(existing_package)
+ .and change(debian_file_metadatum, :file_type).from('unknown').to(expected_file_type.to_s)
+ .and change(debian_file_metadatum, :component).from(nil).to(component_name)
+ end
+
+ context 'when marked as pending_destruction' do
+ it 'does not re-use the existing package' do
+ existing_package.pending_destruction!
+
+ expect { subject.execute }
+ .to change(Packages::Package, :count).by(1)
+ .and not_change(Packages::PackageFile, :count)
+ end
+ end
+ end
+ end
+ end
+
+ context 'without a distribution' do
+ let(:package_file) { incoming.package_files.with_file_name('libsample0_1.2.3~alpha2_amd64.deb').first }
+ let(:component_name) { 'main' }
+
+ before do
+ distribution.destroy!
+ end
+
+ it 'raise ActiveRecord::RecordNotFound', :aggregate_failures do
+ expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
+ expect { subject.execute }
+ .to not_change(Packages::Package, :count)
+ .and not_change(Packages::PackageFile, :count)
+ .and not_change(incoming.package_files, :count)
+ .and raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with package file without Debian metadata' do
+ let!(:package_file) { create(:debian_package_file, without_loaded_metadatum: true) }
+ let(:component_name) { 'main' }
+
+ it 'raise ArgumentError', :aggregate_failures do
+ expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
+ expect { subject.execute }
+ .to not_change(Packages::Package, :count)
+ .and not_change(Packages::PackageFile, :count)
+ .and not_change(incoming.package_files, :count)
+ .and raise_error(ArgumentError, 'package file without Debian metadata')
+ end
+ end
+
+ context 'with already processed package file' do
+ let_it_be(:package_file) { create(:debian_package_file) }
+
+ let(:component_name) { 'main' }
+
+ it 'raise ArgumentError', :aggregate_failures do
+ expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
+ expect { subject.execute }
+ .to not_change(Packages::Package, :count)
+ .and not_change(Packages::PackageFile, :count)
+ .and not_change(incoming.package_files, :count)
+ .and raise_error(ArgumentError, 'already processed package file')
+ end
+ end
+
+ context 'with invalid package file type' do
+ let(:package_file) { incoming.package_files.with_file_name('sample_1.2.3~alpha2.tar.xz').first }
+ let(:component_name) { 'main' }
+
+ it 'raise ArgumentError', :aggregate_failures do
+ expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
+ expect { subject.execute }
+ .to not_change(Packages::Package, :count)
+ .and not_change(Packages::PackageFile, :count)
+ .and not_change(incoming.package_files, :count)
+ .and raise_error(ArgumentError, 'invalid package file type: source')
+ end
+ end
+
+ context 'when creating package fails' do
+ let(:package_file) { incoming.package_files.with_file_name('libsample0_1.2.3~alpha2_amd64.deb').first }
+ let(:component_name) { 'main' }
+
+ before do
+ allow_next_instance_of(::Packages::Debian::FindOrCreatePackageService) do |find_or_create_package_service|
+ allow(find_or_create_package_service)
+ .to receive(:execute).and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
+ end
+ end
+
+ it 're-raise error', :aggregate_failures do
+ expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
+ expect { subject.execute }
+ .to not_change(Packages::Package, :count)
+ .and not_change(Packages::PackageFile, :count)
+ .and not_change(incoming.package_files, :count)
+ .and raise_error(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
+ end
+ end
+ end
+end
diff --git a/spec/services/pages_domains/retry_acme_order_service_spec.rb b/spec/services/pages_domains/retry_acme_order_service_spec.rb
index 601de24e766..3152e05f2f1 100644
--- a/spec/services/pages_domains/retry_acme_order_service_spec.rb
+++ b/spec/services/pages_domains/retry_acme_order_service_spec.rb
@@ -2,21 +2,37 @@
require 'spec_helper'
-RSpec.describe PagesDomains::RetryAcmeOrderService do
- let(:domain) { create(:pages_domain, auto_ssl_enabled: true, auto_ssl_failed: true) }
+RSpec.describe PagesDomains::RetryAcmeOrderService, feature_category: :pages do
+ let_it_be(:project) { create(:project) }
+
+ let(:domain) { create(:pages_domain, project: project, auto_ssl_enabled: true, auto_ssl_failed: true) }
let(:service) { described_class.new(domain) }
it 'clears auto_ssl_failed' do
- expect do
- service.execute
- end.to change { domain.auto_ssl_failed }.from(true).to(false)
+ expect { service.execute }
+ .to change { domain.auto_ssl_failed }
+ .from(true).to(false)
+ .and publish_event(PagesDomains::PagesDomainUpdatedEvent)
+ .with(
+ project_id: project.id,
+ namespace_id: project.namespace.id,
+ root_namespace_id: project.root_namespace.id,
+ domain: domain.domain
+ )
end
- it 'schedules renewal worker' do
+ it 'schedules renewal worker and publish PagesDomainUpdatedEvent event' do
expect(PagesDomainSslRenewalWorker).to receive(:perform_async).with(domain.id).and_return(nil).once
- service.execute
+ expect { service.execute }
+ .to publish_event(PagesDomains::PagesDomainUpdatedEvent)
+ .with(
+ project_id: project.id,
+ namespace_id: project.namespace.id,
+ root_namespace_id: project.root_namespace.id,
+ domain: domain.domain
+ )
end
it "doesn't schedule renewal worker if Let's Encrypt integration is not enabled" do
@@ -24,7 +40,8 @@ RSpec.describe PagesDomains::RetryAcmeOrderService do
expect(PagesDomainSslRenewalWorker).not_to receive(:new)
- service.execute
+ expect { service.execute }
+ .to not_publish_event(PagesDomains::PagesDomainUpdatedEvent)
end
it "doesn't schedule renewal worker if auto ssl has not failed yet" do
@@ -32,6 +49,7 @@ RSpec.describe PagesDomains::RetryAcmeOrderService do
expect(PagesDomainSslRenewalWorker).not_to receive(:new)
- service.execute
+ expect { service.execute }
+ .to not_publish_event(PagesDomains::PagesDomainUpdatedEvent)
end
end
diff --git a/spec/services/personal_access_tokens/revoke_service_spec.rb b/spec/services/personal_access_tokens/revoke_service_spec.rb
index f16b6f00a0a..562d6017405 100644
--- a/spec/services/personal_access_tokens/revoke_service_spec.rb
+++ b/spec/services/personal_access_tokens/revoke_service_spec.rb
@@ -8,7 +8,12 @@ RSpec.describe PersonalAccessTokens::RevokeService do
it { expect(service.token.revoked?).to be true }
it 'logs the event' do
- expect(Gitlab::AppLogger).to receive(:info).with(/PAT REVOCATION: revoked_by: '#{current_user.username}', revoked_for: '#{token.user.username}', token_id: '\d+'/)
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ class: described_class.to_s,
+ message: 'PAT Revoked',
+ revoked_by: revoked_by,
+ revoked_for: token.user.username,
+ token_id: token.id)
subject
end
@@ -29,7 +34,9 @@ RSpec.describe PersonalAccessTokens::RevokeService do
let_it_be(:current_user) { create(:admin) }
let_it_be(:token) { create(:personal_access_token) }
- it_behaves_like 'a successfully revoked token'
+ it_behaves_like 'a successfully revoked token' do
+ let(:revoked_by) { current_user.username }
+ end
end
context 'when admin mode is disabled' do
@@ -52,7 +59,38 @@ RSpec.describe PersonalAccessTokens::RevokeService do
context 'token belongs to current_user' do
let_it_be(:token) { create(:personal_access_token, user: current_user) }
- it_behaves_like 'a successfully revoked token'
+ it_behaves_like 'a successfully revoked token' do
+ let(:revoked_by) { current_user.username }
+ end
+ end
+ end
+
+ context 'when source' do
+ let(:service) { described_class.new(nil, token: token, source: source) }
+
+ let_it_be(:current_user) { nil }
+
+ context 'when source is valid' do
+ let_it_be(:source) { 'secret_detection' }
+ let_it_be(:token) { create(:personal_access_token) }
+
+ it_behaves_like 'a successfully revoked token' do
+ let(:revoked_by) { 'secret_detection' }
+ end
+ end
+
+ context 'when source is invalid' do
+ let_it_be(:source) { 'external_request' }
+ let_it_be(:token) { create(:personal_access_token) }
+
+ it_behaves_like 'an unsuccessfully revoked token'
+ end
+
+ context 'when source is missing' do
+ let_it_be(:source) { nil }
+ let_it_be(:token) { create(:personal_access_token) }
+
+ it_behaves_like 'an unsuccessfully revoked token'
end
end
end
diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb
index edf4bbe0f7f..72bb0adbf56 100644
--- a/spec/services/projects/after_rename_service_spec.rb
+++ b/spec/services/projects/after_rename_service_spec.rb
@@ -9,6 +9,16 @@ RSpec.describe Projects::AfterRenameService do
let!(:full_path_before_rename) { project.full_path }
let!(:path_after_rename) { "#{project.path}-renamed" }
let!(:full_path_after_rename) { "#{project.full_path}-renamed" }
+ let!(:repo_before_rename) { project.repository.raw }
+ let!(:wiki_repo_before_rename) { project.wiki.repository.raw }
+
+ let(:repo_after_rename) do
+ Gitlab::Git::Repository.new(project.repository_storage, "#{full_path_after_rename}.git", nil, nil)
+ end
+
+ let(:wiki_repo_after_rename) do
+ Gitlab::Git::Repository.new(project.repository_storage, "#{full_path_after_rename}.wiki.git", nil, nil)
+ end
describe '#execute' do
context 'using legacy storage' do
@@ -35,13 +45,15 @@ RSpec.describe Projects::AfterRenameService do
.to receive(:rename_project)
.with(path_before_rename, path_after_rename, project.namespace.full_path)
- expect_repository_exist("#{full_path_before_rename}.git")
- expect_repository_exist("#{full_path_before_rename}.wiki.git")
+ expect(repo_before_rename).to exist
+ expect(wiki_repo_before_rename).to exist
service_execute
- expect_repository_exist("#{full_path_after_rename}.git")
- expect_repository_exist("#{full_path_after_rename}.wiki.git")
+ expect(repo_before_rename).not_to exist
+ expect(wiki_repo_before_rename).not_to exist
+ expect(repo_after_rename).to exist
+ expect(wiki_repo_after_rename).to exist
end
context 'container registry with images' do
@@ -212,13 +224,4 @@ RSpec.describe Projects::AfterRenameService do
described_class.new(project, path_before: path_before_rename, full_path_before: full_path_before_rename).execute
end
-
- def expect_repository_exist(full_path_with_extension)
- expect(
- TestEnv.storage_dir_exists?(
- project.repository_storage,
- full_path_with_extension
- )
- ).to be_truthy
- end
end
diff --git a/spec/services/projects/container_repository/destroy_service_spec.rb b/spec/services/projects/container_repository/destroy_service_spec.rb
index 20e75d94e05..0ec0aecaa04 100644
--- a/spec/services/projects/container_repository/destroy_service_spec.rb
+++ b/spec/services/projects/container_repository/destroy_service_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Projects::ContainerRepository::DestroyService do
let!(:repository) { create(:container_repository, :root, project: project) }
it 'does not delete a repository' do
- expect { subject.execute(repository) }.not_to change { ContainerRepository.all.count }
+ expect { subject.execute(repository) }.not_to change { ContainerRepository.count }
end
end
@@ -29,23 +29,62 @@ RSpec.describe Projects::ContainerRepository::DestroyService do
let!(:repository) { create(:container_repository, :root, project: project) }
before do
- stub_container_registry_tags(repository: :any, tags: [])
+ stub_container_registry_tags(repository: :any, tags: %w[latest stable])
end
it 'deletes the repository' do
- expect(repository).to receive(:delete_tags!).and_call_original
- expect { described_class.new(project, user).execute(repository) }.to change { ContainerRepository.all.count }.by(-1)
+ expect_cleanup_tags_service_with(container_repository: repository, return_status: :success)
+ expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1)
end
- context 'when destroy fails' do
- it 'set delete_status' do
+ it 'sends disable_timeout = true as part of the params as default' do
+ expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: true)
+ expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1)
+ end
+
+ it 'sends disable_timeout = false as part of the params if it is set to false' do
+ expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: false)
+ expect { subject.execute(repository, disable_timeout: false) }.to change { ContainerRepository.count }.by(-1)
+ end
+
+ context 'when deleting the tags fails' do
+ it 'sets status as deleted_failed' do
+ expect_cleanup_tags_service_with(container_repository: repository, return_status: :error)
+ allow(Gitlab::AppLogger).to receive(:error).and_call_original
+
+ subject.execute(repository)
+
+ expect(repository).to be_delete_failed
+ expect(Gitlab::AppLogger).to have_received(:error)
+ .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: error in deleting tags")
+ end
+ end
+
+ context 'when destroying the repository fails' do
+ it 'sets status as deleted_failed' do
+ expect_cleanup_tags_service_with(container_repository: repository, return_status: :success)
allow(repository).to receive(:destroy).and_return(false)
+ allow(repository.errors).to receive(:full_messages).and_return(['Error 1', 'Error 2'])
+ allow(Gitlab::AppLogger).to receive(:error).and_call_original
subject.execute(repository)
expect(repository).to be_delete_failed
+ expect(Gitlab::AppLogger).to have_received(:error)
+ .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: Error 1. Error 2")
end
end
+
+ def expect_cleanup_tags_service_with(container_repository:, return_status:, disable_timeout: true)
+ delete_tags_service = instance_double(Projects::ContainerRepository::CleanupTagsService)
+
+ expect(Projects::ContainerRepository::CleanupTagsService).to receive(:new).with(
+ container_repository: container_repository,
+ params: described_class::CLEANUP_TAGS_SERVICE_PARAMS.merge('disable_timeout' => disable_timeout)
+ ).and_return(delete_tags_service)
+
+ expect(delete_tags_service).to receive(:execute).and_return(status: return_status)
+ end
end
end
end
diff --git a/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb
index 59827ea035e..b06a5709bd5 100644
--- a/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb
@@ -49,6 +49,9 @@ RSpec.describe Projects::ContainerRepository::Gitlab::CleanupTagsService do
it_behaves_like 'when regex matching everything is specified',
delete_expectations: [%w[A], %w[Ba Bb], %w[C D], %w[E]]
+ it_behaves_like 'when regex matching everything is specified and latest is not kept',
+ delete_expectations: [%w[latest A], %w[Ba Bb], %w[C D], %w[E]]
+
it_behaves_like 'when delete regex matching specific tags is used'
it_behaves_like 'when delete regex matching specific tags is used with overriding allow regex'
@@ -97,6 +100,19 @@ RSpec.describe Projects::ContainerRepository::Gitlab::CleanupTagsService do
is_expected.to eq(response)
end
+
+ context 'when disable_timeout is set to true' do
+ let(:params) do
+ { 'name_regex_delete' => '.*', 'disable_timeout' => true }
+ end
+
+ it 'does not check if it timed out' do
+ expect(service).not_to receive(:timeout?)
+ end
+
+ it_behaves_like 'when regex matching everything is specified',
+ delete_expectations: [%w[A], %w[Ba Bb], %w[C D], %w[E]]
+ end
end
end
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 8d8907119f0..f03912dba80 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
@@ -24,6 +24,10 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService do
context 'with tags to delete' do
let(:timeout) { 10 }
+ before do
+ stub_application_setting(container_registry_delete_tags_service_timeout: timeout)
+ end
+
it_behaves_like 'deleting tags'
it 'succeeds when tag delete returns 404' do
@@ -48,10 +52,6 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService do
end
end
- before do
- stub_application_setting(container_registry_delete_tags_service_timeout: timeout)
- end
-
context 'with timeout' do
context 'set to a valid value' do
before do
diff --git a/spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb
index 2d034d577ac..7227834b131 100644
--- a/spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb
@@ -51,6 +51,16 @@ RSpec.describe Projects::ContainerRepository::ThirdParty::CleanupTagsService, :c
},
supports_caching: true
+ it_behaves_like 'when regex matching everything is specified and latest is not kept',
+ delete_expectations: [%w[A Ba Bb C D E latest]],
+ service_response_extra: {
+ before_truncate_size: 7,
+ after_truncate_size: 7,
+ before_delete_size: 7,
+ cached_tags_count: 0
+ },
+ supports_caching: true
+
it_behaves_like 'when delete regex matching specific tags is used',
service_response_extra: {
before_truncate_size: 2,
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 9c8aeb5cf7b..f42ab198a04 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -145,6 +145,20 @@ RSpec.describe Projects::CreateService, '#execute' do
end
end
end
+
+ context 'when the passed in namespace is for a bot user' do
+ let(:bot_user) { create(:user, :project_bot) }
+ let(:opts) do
+ { name: project_name, namespace_id: bot_user.namespace.id }
+ end
+
+ it 'raises an error' do
+ project = create_project(bot_user, opts)
+
+ expect(project.errors.errors.length).to eq 1
+ expect(project.errors.messages[:namespace].first).to eq(("is not valid"))
+ end
+ end
end
describe 'after create actions' do
@@ -908,27 +922,6 @@ RSpec.describe Projects::CreateService, '#execute' do
end
end
- it_behaves_like 'measurable service' do
- before do
- opts.merge!(
- current_user: user,
- path: 'foo'
- )
- end
-
- let(:base_log_data) do
- {
- class: Projects::CreateService.name,
- current_user: user.name,
- project_full_path: "#{user.namespace.full_path}/#{opts[:path]}"
- }
- end
-
- after do
- create_project(user, opts)
- end
- end
-
context 'with specialized project_authorization workers' do
let_it_be(:other_user) { create(:user) }
let_it_be(:group) { create(:group) }
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index f7f02769f6a..ff2de45661f 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publisher do
include ProjectForksHelper
+ include BatchDestroyDependentAssociationsHelper
let_it_be(:user) { create(:user) }
@@ -331,8 +332,8 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
context 'when image repository deletion succeeds' do
it 'removes tags' do
- expect_any_instance_of(ContainerRepository)
- .to receive(:delete_tags!).and_return(true)
+ expect_any_instance_of(Projects::ContainerRepository::CleanupTagsService)
+ .to receive(:execute).and_return({ status: :success })
destroy_project(project, user)
end
@@ -340,8 +341,8 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
context 'when image repository deletion fails' do
it 'raises an exception' do
- expect_any_instance_of(ContainerRepository)
- .to receive(:delete_tags!).and_raise(RuntimeError)
+ expect_any_instance_of(Projects::ContainerRepository::CleanupTagsService)
+ .to receive(:execute).and_raise(RuntimeError)
expect(destroy_project(project, user)).to be false
end
@@ -548,6 +549,30 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
end
end
+ context 'associations destoyed in batches' do
+ let!(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:label) { create(:label, project: project) }
+
+ it 'destroys the associations marked as `dependent: :destroy`, in batches' do
+ query_recorder = ActiveRecord::QueryRecorder.new do
+ destroy_project(project, user, {})
+ end
+
+ expect(project.merge_requests).to be_empty
+ expect(project.issues).to be_empty
+ expect(project.labels).to be_empty
+
+ expected_queries = [
+ delete_in_batches_regexps(:merge_requests, :target_project_id, project, [merge_request]),
+ delete_in_batches_regexps(:issues, :project_id, project, [issue]),
+ delete_in_batches_regexps(:labels, :project_id, project, [label])
+ ].flatten
+
+ expect(query_recorder.log).to include(*expected_queries)
+ end
+ end
+
def destroy_project(project, user, params = {})
described_class.new(project, user, params).public_send(async ? :async_execute : :execute)
end
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
index 7d4fce814f5..f158b11a9fa 100644
--- a/spec/services/projects/download_service_spec.rb
+++ b/spec/services/projects/download_service_spec.rb
@@ -21,8 +21,8 @@ RSpec.describe Projects::DownloadService do
context 'for URLs that are on the whitelist' do
before do
# `ssrf_filter` resolves the hostname. See https://github.com/carrierwaveuploader/carrierwave/commit/91714adda998bc9e8decf5b1f5d260d808761304
- stub_request(:get, %r{http://[\d\.]+/rails_sample.jpg}).to_return(body: File.read(Rails.root + 'spec/fixtures/rails_sample.jpg'))
- stub_request(:get, %r{http://[\d\.]+/doc_sample.txt}).to_return(body: File.read(Rails.root + 'spec/fixtures/doc_sample.txt'))
+ stub_request(:get, %r{http://[\d.]+/rails_sample.jpg}).to_return(body: File.read(Rails.root + 'spec/fixtures/rails_sample.jpg'))
+ stub_request(:get, %r{http://[\d.]+/doc_sample.txt}).to_return(body: File.read(Rails.root + 'spec/fixtures/doc_sample.txt'))
end
context 'an image file' do
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index 285687505e9..2c1ebe27014 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -216,24 +216,9 @@ RSpec.describe Projects::ImportExport::ExportService do
it 'fails' do
expected_message =
"User with ID: %s does not have required permissions for Project: %s with ID: %s" %
- [another_user.id, project.name, project.id]
+ [another_user.id, project.name, project.id]
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message)
end
end
-
- it_behaves_like 'measurable service' do
- let(:base_log_data) do
- {
- class: described_class.name,
- current_user: user.name,
- project_full_path: project.full_path,
- file_path: shared.export_path
- }
- end
-
- after do
- service.execute(after_export_strategy)
- end
- end
end
end
diff --git a/spec/services/projects/import_export/parallel_export_service_spec.rb b/spec/services/projects/import_export/parallel_export_service_spec.rb
new file mode 100644
index 00000000000..b9f2867077c
--- /dev/null
+++ b/spec/services/projects/import_export/parallel_export_service_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ImportExport::ParallelExportService, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+
+ let(:export_job) { create(:project_export_job) }
+ let(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
+ let(:project) { export_job.project }
+
+ before do
+ allow_next_instance_of(Gitlab::ImportExport::Project::ExportedRelationsMerger) do |saver|
+ allow(saver).to receive(:save).and_return(true)
+ end
+
+ allow_next_instance_of(Gitlab::ImportExport::VersionSaver) do |saver|
+ allow(saver).to receive(:save).and_return(true)
+ end
+ end
+
+ describe '#execute' do
+ subject(:service) { described_class.new(export_job, user, after_export_strategy) }
+
+ it 'creates a project export archive file' do
+ expect(Gitlab::ImportExport::Saver).to receive(:save)
+ .with(exportable: project, shared: project.import_export_shared)
+
+ service.execute
+ end
+
+ it 'logs export progress' do
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)
+
+ logger = service.instance_variable_get(:@logger)
+ messages = [
+ 'Parallel project export started',
+ 'Parallel project export - Gitlab::ImportExport::VersionSaver saver started',
+ 'Parallel project export - Gitlab::ImportExport::Project::ExportedRelationsMerger saver started',
+ 'Parallel project export finished successfully'
+ ]
+ messages.each do |message|
+ expect(logger).to receive(:info).ordered.with(hash_including(message: message))
+ end
+
+ service.execute
+ end
+
+ it 'executes after export stragegy on export success' do
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)
+
+ expect(after_export_strategy).to receive(:execute)
+
+ service.execute
+ end
+
+ it 'ensures files are cleaned up' do
+ shared = project.import_export_shared
+ FileUtils.mkdir_p(shared.archive_path)
+ FileUtils.mkdir_p(shared.export_path)
+
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_raise(StandardError)
+
+ expect { service.execute }.to raise_error(StandardError)
+
+ expect(File.exist?(shared.export_path)).to eq(false)
+ expect(File.exist?(shared.archive_path)).to eq(false)
+ end
+
+ context 'when export fails' do
+ it 'notifies the error to the user' do
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(false)
+
+ allow(project.import_export_shared).to receive(:errors).and_return(['Error'])
+
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:project_not_exported).with(project, user, ['Error'])
+ end
+
+ service.execute
+ end
+ end
+
+ context 'when after export stragegy fails' do
+ it 'notifies the error to the user' do
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)
+ allow(after_export_strategy).to receive(:execute).and_return(false)
+ allow(project.import_export_shared).to receive(:errors).and_return(['Error'])
+
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:project_not_exported).with(project, user, ['Error'])
+ end
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index b3f8980a7bd..bb11b2e617e 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -419,25 +419,5 @@ RSpec.describe Projects::ImportService do
end
end
end
-
- it_behaves_like 'measurable service' do
- let(:base_log_data) do
- {
- class: described_class.name,
- current_user: user.name,
- project_full_path: project.full_path,
- import_type: project.import_type,
- file_path: project.import_source
- }
- end
-
- before do
- project.import_type = 'github'
- end
-
- after do
- subject.execute
- end
- end
end
end
diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
index d472d6493c3..80b3c4d0403 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService do
]
end
- subject { described_class.new(project, remote_uri: remote_uri) }
+ subject(:service) { described_class.new(project, remote_uri: remote_uri) }
before do
allow(project).to receive(:lfs_enabled?).and_return(true)
@@ -48,19 +48,24 @@ RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService do
allow(Gitlab::HTTP).to receive(:post).and_return(response)
end
- describe '#execute' do
- let(:download_objects) { subject.execute(new_oids) }
-
+ describe '#each_link' do
it 'retrieves each download link of every non existent lfs object' do
- download_objects.each do |lfs_download_object|
- expect(lfs_download_object.link).to eq "#{import_url}/gitlab-lfs/objects/#{lfs_download_object.oid}"
- end
+ links = []
+ service.each_link(new_oids) { |lfs_download_object| links << lfs_download_object.link }
+
+ expect(links).to contain_exactly(
+ "#{import_url}/gitlab-lfs/objects/oid1",
+ "#{import_url}/gitlab-lfs/objects/oid2"
+ )
end
it 'stores headers' do
- download_objects.each do |lfs_download_object|
- expect(lfs_download_object.headers).to eq(headers)
+ expected_headers = []
+ service.each_link(new_oids) do |lfs_download_object|
+ expected_headers << lfs_download_object.headers
end
+
+ expect(expected_headers).to contain_exactly(headers, headers)
end
context 'when lfs objects size is larger than the batch size' do
@@ -97,10 +102,13 @@ RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService do
stub_successful_request([data[4]])
end
- it 'retreives them in batches' do
- subject.execute(new_oids).each do |lfs_download_object|
+ it 'retrieves them in batches' do
+ checksum = 0
+ service.each_link(new_oids) do |lfs_download_object|
expect(lfs_download_object.link).to eq "#{import_url}/gitlab-lfs/objects/#{lfs_download_object.oid}"
+ checksum += 1
end
+ expect(checksum).to eq new_oids.size
end
end
@@ -127,9 +135,12 @@ RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService do
an_instance_of(error_class), project_id: project.id, batch_size: 5, oids_count: 5
)
- subject.execute(new_oids).each do |lfs_download_object|
+ checksum = 0
+ service.each_link(new_oids) do |lfs_download_object|
expect(lfs_download_object.link).to eq "#{import_url}/gitlab-lfs/objects/#{lfs_download_object.oid}"
+ checksum += 1
end
+ expect(checksum).to eq new_oids.size
end
end
@@ -153,7 +164,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService do
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
an_instance_of(error_class), project_id: project.id, batch_size: 2, oids_count: 5
)
- expect { subject.execute(new_oids) }.to raise_error(described_class::DownloadLinksError)
+ expect { service.each_link(new_oids) }.to raise_error(described_class::DownloadLinksError)
end
end
end
@@ -165,21 +176,23 @@ RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService do
let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git' }
it 'adds credentials to the download_link' do
- result = subject.execute(new_oids)
-
- result.each do |lfs_download_object|
+ checksum = 0
+ service.each_link(new_oids) do |lfs_download_object|
expect(lfs_download_object.link.starts_with?('http://user:password@')).to be_truthy
+ checksum += 1
end
+ expect(checksum).to eq new_oids.size
end
end
context 'when lfs_endpoint does not have any credentials' do
it 'does not add any credentials' do
- result = subject.execute(new_oids)
-
- result.each do |lfs_download_object|
+ checksum = 0
+ service.each_link(new_oids) do |lfs_download_object|
expect(lfs_download_object.link.starts_with?('http://user:password@')).to be_falsey
+ checksum += 1
end
+ expect(checksum).to eq new_oids.size
end
end
end
@@ -189,17 +202,18 @@ RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService do
let(:lfs_endpoint) { "#{import_url_with_credentials}/info/lfs/objects/batch" }
it 'downloads without any credentials' do
- result = subject.execute(new_oids)
-
- result.each do |lfs_download_object|
+ checksum = 0
+ service.each_link(new_oids) do |lfs_download_object|
expect(lfs_download_object.link.starts_with?('http://user:password@')).to be_falsey
+ checksum += 1
end
+ expect(checksum).to eq new_oids.size
end
end
end
end
- describe '#get_download_links' do
+ describe '#download_links_for' do
context 'if request fails' do
before do
request_timeout_net_response = Net::HTTPRequestTimeout.new('', '', '')
@@ -208,7 +222,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService do
end
it 'raises an error' do
- expect { subject.send(:get_download_links, new_oids) }.to raise_error(described_class::DownloadLinksError)
+ expect { subject.send(:download_links_for, new_oids) }.to raise_error(described_class::DownloadLinksError)
end
end
@@ -218,7 +232,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService do
allow(response).to receive(:body).and_return(body)
allow(Gitlab::HTTP).to receive(:post).and_return(response)
- expect { subject.send(:get_download_links, new_oids) }.to raise_error(described_class::DownloadLinksError)
+ expect { subject.send(:download_links_for, new_oids) }.to raise_error(described_class::DownloadLinksError)
end
end
diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
index 6c7164c5e06..c815ad38843 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadService do
end
end
- context 'when file download fails' do
+ context 'when file downloading response code is not success' do
before do
allow(Gitlab::HTTP).to receive(:get).and_return(code: 500, 'success?' => false)
end
@@ -122,6 +122,20 @@ RSpec.describe Projects::LfsPointers::LfsDownloadService do
end
end
+ context 'when file downloading request timeout few times' do
+ before do
+ allow(Gitlab::HTTP).to receive(:get).and_raise(Net::OpenTimeout)
+ end
+
+ it_behaves_like 'no lfs object is created'
+
+ it 'retries to get LFS object 3 times before raising exception' do
+ subject.execute
+
+ expect(Gitlab::HTTP).to have_received(:get).exactly(3).times
+ end
+ end
+
context 'when file download returns a redirect' do
let(:redirect_link) { 'http://external-link' }
diff --git a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
index b36b0b8d6b2..32b86ade81e 100644
--- a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
@@ -5,7 +5,12 @@ RSpec.describe Projects::LfsPointers::LfsImportService do
let(:project) { create(:project) }
let(:user) { project.creator }
let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
- let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
+ let(:oid_download_links) do
+ [
+ { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1" },
+ { 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" }
+ ]
+ end
subject { described_class.new(project, user) }
@@ -17,7 +22,8 @@ RSpec.describe Projects::LfsPointers::LfsImportService do
it 'downloads lfs objects' do
service = double
expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |instance|
- expect(instance).to receive(:execute).and_return(oid_download_links)
+ expect(instance).to receive(:each_list_item)
+ .and_yield(oid_download_links[0]).and_yield(oid_download_links[1])
end
expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice
expect(service).to receive(:execute).twice
@@ -30,7 +36,7 @@ RSpec.describe Projects::LfsPointers::LfsImportService do
context 'when no downloadable lfs object links' do
it 'does not call LfsDownloadService' do
expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |instance|
- expect(instance).to receive(:execute).and_return({})
+ expect(instance).to receive(:each_list_item)
end
expect(Projects::LfsPointers::LfsDownloadService).not_to receive(:new)
@@ -44,7 +50,7 @@ RSpec.describe Projects::LfsPointers::LfsImportService do
it 'returns error' do
error_message = "error message"
expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |instance|
- expect(instance).to receive(:execute).and_raise(StandardError, error_message)
+ expect(instance).to receive(:each_list_item).and_raise(StandardError, error_message)
end
result = subject.execute
diff --git a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
index adcc2b85706..59eb1ed7a29 100644
--- a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
@@ -9,7 +9,13 @@ RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do
let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) }
let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h }
let(:oids) { { 'oid1' => 123, 'oid2' => 125 } }
- let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
+ let(:oid_download_links) do
+ [
+ { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1" },
+ { 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" }
+ ]
+ end
+
let(:all_oids) { existing_lfs_objects.merge(oids) }
let(:remote_uri) { URI.parse(lfs_endpoint) }
@@ -21,17 +27,24 @@ RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do
allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids)
end
- describe '#execute' do
+ describe '#each_list_item' do
context 'when no lfs pointer is linked' do
before do
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
- expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:each_link).with(oids)
+ .and_yield(oid_download_links[0])
+ .and_yield(oid_download_links[1])
end
it 'retrieves all lfs pointers in the project repository' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:new).with(project, remote_uri: URI.parse(default_endpoint))
+ .and_call_original
expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute)
- subject.execute
+ checksum = 0
+ subject.each_list_item { |lfs_object| checksum += 1 }
+ expect(checksum).to eq 2
end
context 'when no LFS objects exist' do
@@ -40,17 +53,23 @@ RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do
end
it 'retrieves all LFS objects' do
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids)
+ expect(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:each_link).with(all_oids)
- subject.execute
+ subject.each_list_item {}
end
end
context 'when some LFS objects already exist' do
it 'retrieves the download links of non-existent objects' do
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids)
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:each_link).with(oids)
- subject.execute
+ checksum = 0
+ subject.each_list_item { |lfs_object| checksum += 1 }
+ expect(checksum).to eq 2
end
end
end
@@ -62,16 +81,15 @@ RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do
context 'when url points to the same import url host' do
let(:lfs_endpoint) { "#{import_url}/different_endpoint" }
- let(:service) { double }
-
- before do
- allow(service).to receive(:execute)
- end
+ let(:service) { instance_double(Projects::LfsPointers::LfsDownloadLinkListService, each_link: nil) }
it 'downloads lfs object using the new endpoint' do
- expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service)
+ expect(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:new)
+ .with(project, remote_uri: remote_uri)
+ .and_return(service)
- subject.execute
+ subject.each_list_item {}
end
context 'when import url has credentials' do
@@ -79,10 +97,14 @@ RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do
it 'adds the credentials to the new endpoint' do
expect(Projects::LfsPointers::LfsDownloadLinkListService)
- .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint"))
+ .to receive(:new)
+ .with(
+ project,
+ remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint")
+ )
.and_return(service)
- subject.execute
+ subject.each_list_item {}
end
context 'when url has its own credentials' do
@@ -93,7 +115,7 @@ RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do
.to receive(:new).with(project, remote_uri: remote_uri)
.and_return(service)
- subject.execute
+ subject.each_list_item {}
end
end
end
@@ -105,7 +127,7 @@ RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do
it 'disables lfs from the project' do
expect(project.lfs_enabled?).to be_truthy
- subject.execute
+ subject.each_list_item {}
expect(project.lfs_enabled?).to be_falsey
end
@@ -113,7 +135,7 @@ RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do
it 'does not download anything' do
expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute)
- subject.execute
+ subject.each_list_item {}
end
end
end
diff --git a/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb b/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb
index 6a715312097..a3cff345f68 100644
--- a/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb
+++ b/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb
@@ -28,6 +28,7 @@ RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsService, :clean_gitl
end
let(:now) { Time.zone.now }
+ let(:statistics) { project.statistics }
around do |example|
freeze_time { example.run }
@@ -45,7 +46,7 @@ RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsService, :clean_gitl
end
it 'increments the counter attribute by the total size of the current batch of artifacts' do
- expect { service.execute }.to change { project.statistics.get_counter_value(:build_artifacts_size) }.to(3)
+ expect { service.execute }.to change { statistics.counter(:build_artifacts_size).get }.to(3)
end
it 'updates the last_job_artifact_id to the ID of the last artifact from the batch' do
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 8f505c31c5a..4d75786a4c3 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -263,7 +263,7 @@ RSpec.describe Projects::TransferService do
end
context 'when transfer fails' do
- let!(:original_path) { project_path(project) }
+ let!(:original_path) { project.repository.relative_path }
def attempt_project_transfer(&block)
expect do
@@ -277,21 +277,11 @@ RSpec.describe Projects::TransferService do
expect_any_instance_of(Labels::TransferService).to receive(:execute).and_raise(ActiveRecord::StatementInvalid, "PG ERROR")
end
- def project_path(project)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- project.repository.path_to_repo
- end
- end
-
- def current_path
- project_path(project)
- end
-
it 'rolls back repo location' do
attempt_project_transfer
expect(project.repository.raw.exists?).to be(true)
- expect(original_path).to eq current_path
+ expect(original_path).to eq project.repository.relative_path
end
it 'rolls back project full path in gitaly' do
@@ -462,6 +452,22 @@ RSpec.describe Projects::TransferService do
end
end
+ context 'target namespace belongs to bot user', :enable_admin_mode do
+ let(:bot) { create(:user, :project_bot) }
+ let(:target) { bot.namespace }
+ let(:executor) { create(:user, :admin) }
+
+ it 'does not allow project transfer' do
+ namespace = project.namespace
+
+ transfer_result = execute_transfer
+
+ expect(transfer_result).to eq false
+ expect(project.namespace).to eq(namespace)
+ expect(project.errors[:new_namespace]).to include("You don't have permission to transfer projects into that namespace.")
+ end
+ end
+
context 'when user does not own the project' do
let(:project) { create(:project, :repository, :legacy_storage) }
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index a69db3b9970..d908a169898 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -320,10 +320,11 @@ RSpec.describe Projects::UpdatePagesService do
end
context 'when retrying the job' do
+ let(:stage) { create(:ci_stage, position: 1_000_000, name: 'deploy', pipeline: pipeline) }
let!(:older_deploy_job) do
create(:generic_commit_status, :failed, pipeline: pipeline,
ref: build.ref,
- stage: 'deploy',
+ ci_stage: stage,
name: 'pages:deploy')
end
@@ -337,13 +338,15 @@ RSpec.describe Projects::UpdatePagesService do
expect(execute).to eq(:success)
expect(older_deploy_job.reload).to be_retried
+ expect(deploy_status.ci_stage).to eq(stage)
+ expect(deploy_status.stage_idx).to eq(stage.position)
end
end
private
def deploy_status
- GenericCommitStatus.find_by(name: 'pages:deploy')
+ GenericCommitStatus.where(name: 'pages:deploy').last
end
def execute
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 7d8951bf111..3cda6bc2627 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-
require 'spec_helper'
RSpec.describe Projects::UpdateService do
@@ -303,6 +302,25 @@ RSpec.describe Projects::UpdateService do
expect(project.default_branch).to eq 'master'
expect(project.previous_default_branch).to be_nil
end
+
+ context 'when repository has an ambiguous branch named "HEAD"' do
+ before do
+ allow(project.repository.raw).to receive(:write_ref).and_return(false)
+ allow(project.repository).to receive(:branch_names) { %w[fix master main HEAD] }
+ end
+
+ it 'returns an error to the user' do
+ result = update_project(project, admin, default_branch: 'fix')
+
+ expect(result).to include(status: :error)
+ expect(result[:message]).to include("Could not set the default branch. Do you have a branch named 'HEAD' in your repository?")
+
+ project.reload
+
+ expect(project.default_branch).to eq 'master'
+ expect(project.previous_default_branch).to be_nil
+ end
+ end
end
context 'when we update project but not enabling a wiki' do
@@ -422,25 +440,6 @@ RSpec.describe Projects::UpdateService do
expect(feature.feature_flags_access_level).not_to eq(ProjectFeature::DISABLED)
expect(feature.environments_access_level).not_to eq(ProjectFeature::DISABLED)
end
-
- context 'when split_operations_visibility_permissions feature is disabled' do
- before do
- stub_feature_flags(split_operations_visibility_permissions: false)
- end
-
- it 'syncs the changes to the related fields' do
- result = update_project(project, user, project_feature_attributes: feature_params)
-
- expect(result).to eq({ status: :success })
- feature = project.project_feature
-
- expect(feature.operations_access_level).to eq(ProjectFeature::DISABLED)
- expect(feature.monitor_access_level).to eq(ProjectFeature::DISABLED)
- expect(feature.infrastructure_access_level).to eq(ProjectFeature::DISABLED)
- expect(feature.feature_flags_access_level).to eq(ProjectFeature::DISABLED)
- expect(feature.environments_access_level).to eq(ProjectFeature::DISABLED)
- end
- end
end
context 'when updating a project that contains container images' do
diff --git a/spec/services/protected_branches/api_service_spec.rb b/spec/services/protected_branches/api_service_spec.rb
index 94484f5a7b9..c98e253267b 100644
--- a/spec/services/protected_branches/api_service_spec.rb
+++ b/spec/services/protected_branches/api_service_spec.rb
@@ -3,32 +3,55 @@
require 'spec_helper'
RSpec.describe ProtectedBranches::ApiService do
- let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user, maintainer_projects: [project]) }
-
- it 'creates a protected branch with prefilled defaults' do
- expect(::ProtectedBranches::CreateService).to receive(:new).with(
- project, user, hash_including(
- push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }],
- merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }]
- )
- ).and_call_original
-
- expect(described_class.new(project, user, { name: 'new name' }).create).to be_valid
+ shared_examples 'execute with entity' do
+ it 'creates a protected branch with prefilled defaults' do
+ expect(::ProtectedBranches::CreateService).to receive(:new).with(
+ entity, user, hash_including(
+ push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }],
+ merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }]
+ )
+ ).and_call_original
+
+ expect(described_class.new(entity, user, { name: 'new name' }).create).to be_valid
+ end
+
+ it 'updates a protected branch without prefilled defaults' do
+ expect(::ProtectedBranches::UpdateService).to receive(:new).with(
+ entity, user, hash_including(
+ push_access_levels_attributes: [],
+ merge_access_levels_attributes: []
+ )
+ ).and_call_original
+
+ expect do
+ expect(described_class.new(entity, user, { name: 'new name' }).update(protected_branch)).to be_valid
+ end.not_to change { protected_branch.reload.allow_force_push }
+ end
+ end
+
+ context 'with entity project' do
+ let_it_be_with_reload(:entity) { create(:project) }
+ let_it_be_with_reload(:protected_branch) { create(:protected_branch, project: entity, allow_force_push: true) }
+ let(:user) { entity.first_owner }
+
+ it_behaves_like 'execute with entity'
end
- it 'updates a protected branch without prefilled defaults' do
- protected_branch = create(:protected_branch, project: project, allow_force_push: true)
+ context 'with entity group' do
+ let_it_be_with_reload(:entity) { create(:group) }
+ let_it_be_with_reload(:user) { create(:user) }
+ let_it_be_with_reload(:protected_branch) do
+ create(:protected_branch, group: entity, project: nil, allow_force_push: true)
+ end
- expect(::ProtectedBranches::UpdateService).to receive(:new).with(
- project, user, hash_including(
- push_access_levels_attributes: [],
- merge_access_levels_attributes: []
- )
- ).and_call_original
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :update_protected_branch, protected_branch).and_return(true)
+ allow(Ability)
+ .to receive(:allowed?)
+ .with(user, :create_protected_branch, instance_of(ProtectedBranch))
+ .and_return(true)
+ end
- expect do
- expect(described_class.new(project, user, { name: 'new name' }).update(protected_branch)).to be_valid
- end.not_to change { protected_branch.reload.allow_force_push }
+ it_behaves_like 'execute with entity'
end
end
diff --git a/spec/services/protected_branches/cache_service_spec.rb b/spec/services/protected_branches/cache_service_spec.rb
index d7a3258160b..ea434922661 100644
--- a/spec/services/protected_branches/cache_service_spec.rb
+++ b/spec/services/protected_branches/cache_service_spec.rb
@@ -4,122 +4,150 @@
require 'spec_helper'
RSpec.describe ProtectedBranches::CacheService, :clean_gitlab_redis_cache do
- subject(:service) { described_class.new(project, user) }
+ shared_examples 'execute with entity' do
+ subject(:service) { described_class.new(entity, user) }
- let_it_be(:project) { create(:project) }
- let_it_be(:user) { project.first_owner }
+ let(:immediate_expiration) { 0 }
- let(:immediate_expiration) { 0 }
-
- describe '#fetch' do
- it 'caches the value' do
- expect(service.fetch('main') { true }).to eq(true)
- expect(service.fetch('not-found') { false }).to eq(false)
-
- # Uses cached values
- expect(service.fetch('main') { false }).to eq(true)
- expect(service.fetch('not-found') { true }).to eq(false)
- end
-
- it 'sets expiry on the key' do
- stub_const("#{described_class.name}::CACHE_EXPIRE_IN", immediate_expiration)
-
- expect(service.fetch('main') { true }).to eq(true)
- expect(service.fetch('not-found') { false }).to eq(false)
-
- expect(service.fetch('main') { false }).to eq(false)
- expect(service.fetch('not-found') { true }).to eq(true)
- end
-
- it 'does not set an expiry on the key after the hash is already created' do
- expect(service.fetch('main') { true }).to eq(true)
+ describe '#fetch' do
+ it 'caches the value' do
+ expect(service.fetch('main') { true }).to eq(true)
+ expect(service.fetch('not-found') { false }).to eq(false)
- stub_const("#{described_class.name}::CACHE_EXPIRE_IN", immediate_expiration)
+ # Uses cached values
+ expect(service.fetch('main') { false }).to eq(true)
+ expect(service.fetch('not-found') { true }).to eq(false)
+ end
- expect(service.fetch('not-found') { false }).to eq(false)
+ it 'sets expiry on the key' do
+ stub_const("#{described_class.name}::CACHE_EXPIRE_IN", immediate_expiration)
- expect(service.fetch('main') { false }).to eq(true)
- expect(service.fetch('not-found') { true }).to eq(false)
- end
+ expect(service.fetch('main') { true }).to eq(true)
+ expect(service.fetch('not-found') { false }).to eq(false)
- context 'when CACHE_LIMIT is exceeded' do
- before do
- stub_const("#{described_class.name}::CACHE_LIMIT", 2)
+ expect(service.fetch('main') { false }).to eq(false)
+ expect(service.fetch('not-found') { true }).to eq(true)
end
- it 'recreates cache' do
+ it 'does not set an expiry on the key after the hash is already created' do
expect(service.fetch('main') { true }).to eq(true)
+
+ stub_const("#{described_class.name}::CACHE_EXPIRE_IN", immediate_expiration)
+
expect(service.fetch('not-found') { false }).to eq(false)
- # Uses cached values
expect(service.fetch('main') { false }).to eq(true)
expect(service.fetch('not-found') { true }).to eq(false)
+ end
- # Overflow
- expect(service.fetch('new-branch') { true }).to eq(true)
+ context 'when CACHE_LIMIT is exceeded' do
+ before do
+ stub_const("#{described_class.name}::CACHE_LIMIT", 2)
+ end
- # Refreshes values
- expect(service.fetch('main') { false }).to eq(false)
- expect(service.fetch('not-found') { true }).to eq(true)
- end
- end
+ it 'recreates cache' do
+ expect(service.fetch('main') { true }).to eq(true)
+ expect(service.fetch('not-found') { false }).to eq(false)
+
+ # Uses cached values
+ expect(service.fetch('main') { false }).to eq(true)
+ expect(service.fetch('not-found') { true }).to eq(false)
- context 'when dry_run is on' do
- it 'does not use cached value' do
- expect(service.fetch('main', dry_run: true) { true }).to eq(true)
- expect(service.fetch('main', dry_run: true) { false }).to eq(false)
+ # Overflow
+ expect(service.fetch('new-branch') { true }).to eq(true)
+
+ # Refreshes values
+ expect(service.fetch('main') { false }).to eq(false)
+ expect(service.fetch('not-found') { true }).to eq(true)
+ end
end
- context 'when cache mismatch' do
- it 'logs an error' do
+ context 'when dry_run is on' do
+ it 'does not use cached value' do
expect(service.fetch('main', dry_run: true) { true }).to eq(true)
+ expect(service.fetch('main', dry_run: true) { false }).to eq(false)
+ end
+
+ context 'when cache mismatch' do
+ it 'logs an error' do
+ expect(service.fetch('main', dry_run: true) { true }).to eq(true)
+
+ expect(Gitlab::AppLogger).to receive(:error).with(
+ {
+ 'class' => described_class.name,
+ 'message' => /Cache mismatch/,
+ 'record_class' => entity.class.name,
+ 'record_id' => entity.id,
+ 'record_path' => entity.full_path
+ }
+ )
+
+ expect(service.fetch('main', dry_run: true) { false }).to eq(false)
+ end
+ end
- expect(Gitlab::AppLogger).to receive(:error).with(
- {
- 'class' => described_class.name,
- 'message' => /Cache mismatch/,
- 'project_id' => project.id,
- 'project_path' => project.full_path
- }
- )
+ context 'when cache matches' do
+ it 'does not log an error' do
+ expect(service.fetch('main', dry_run: true) { true }).to eq(true)
- expect(service.fetch('main', dry_run: true) { false }).to eq(false)
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect(service.fetch('main', dry_run: true) { true }).to eq(true)
+ end
end
end
+ end
- context 'when cache matches' do
- it 'does not log an error' do
- expect(service.fetch('main', dry_run: true) { true }).to eq(true)
+ describe '#refresh' do
+ it 'clears cached values' do
+ expect(service.fetch('main') { true }).to eq(true)
+ expect(service.fetch('not-found') { false }).to eq(false)
- expect(Gitlab::AppLogger).not_to receive(:error)
+ service.refresh
- expect(service.fetch('main', dry_run: true) { true }).to eq(true)
+ # Recreates cache
+ expect(service.fetch('main') { false }).to eq(false)
+ expect(service.fetch('not-found') { true }).to eq(true)
+ end
+ end
+
+ describe 'metrics' do
+ it 'records hit ratio metrics' do
+ expect_next_instance_of(Gitlab::Cache::Metrics) do |metrics|
+ expect(metrics).to receive(:increment_cache_miss).once
+ expect(metrics).to receive(:increment_cache_hit).exactly(4).times
end
+
+ 5.times { service.fetch('main') { true } }
end
end
end
- describe '#refresh' do
- it 'clears cached values' do
- expect(service.fetch('main') { true }).to eq(true)
- expect(service.fetch('not-found') { false }).to eq(false)
+ context 'with entity project' do
+ let_it_be_with_reload(:entity) { create(:project) }
+ let(:user) { entity.first_owner }
- service.refresh
+ it_behaves_like 'execute with entity'
+ end
+
+ context 'with entity group' do
+ let_it_be_with_reload(:entity) { create(:group) }
+ let_it_be_with_reload(:user) { create(:user) }
- # Recreates cache
- expect(service.fetch('main') { false }).to eq(false)
- expect(service.fetch('not-found') { true }).to eq(true)
+ before do
+ entity.add_owner(user)
end
- end
- describe 'metrics' do
- it 'records hit ratio metrics' do
- expect_next_instance_of(Gitlab::Cache::Metrics) do |metrics|
- expect(metrics).to receive(:increment_cache_miss).once
- expect(metrics).to receive(:increment_cache_hit).exactly(4).times
+ context 'when feature flag enabled' do
+ it_behaves_like 'execute with entity'
+ end
+
+ context 'when feature flag disabled' do
+ before do
+ stub_feature_flags(group_protected_branches: false)
end
- 5.times { service.fetch('main') { true } }
+ it_behaves_like 'execute with entity'
end
end
end
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index b42524e761c..9c8fe769ed8 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -3,70 +3,75 @@
require 'spec_helper'
RSpec.describe ProtectedBranches::CreateService do
- let_it_be_with_reload(:project) { create(:project) }
-
- let(:user) { project.first_owner }
- let(:params) do
- {
- name: name,
- merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }],
- push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }]
- }
- end
-
- subject(:service) { described_class.new(project, user, params) }
-
- describe '#execute' do
- let(:name) { 'master' }
-
- it 'creates a new protected branch' do
- expect { service.execute }.to change(ProtectedBranch, :count).by(1)
- expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
- expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
+ shared_examples 'execute with entity' do
+ let(:params) do
+ {
+ name: name,
+ merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }],
+ push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }]
+ }
end
- it 'refreshes the cache' do
- expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service|
- expect(cache_service).to receive(:refresh)
- end
+ subject(:service) { described_class.new(entity, user, params) }
- service.execute
- end
-
- context 'when protecting a branch with a name that contains HTML tags' do
- let(:name) { 'foo<b>bar<\b>' }
+ describe '#execute' do
+ let(:name) { 'master' }
it 'creates a new protected branch' do
expect { service.execute }.to change(ProtectedBranch, :count).by(1)
- expect(project.protected_branches.last.name).to eq(name)
+ expect(entity.protected_branches.last.push_access_levels.map(&:access_level)).to match_array([Gitlab::Access::MAINTAINER])
+ expect(entity.protected_branches.last.merge_access_levels.map(&:access_level)).to match_array([Gitlab::Access::MAINTAINER])
end
- end
- context 'when user does not have permission' do
- let(:user) { create(:user) }
+ it 'refreshes the cache' do
+ expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service|
+ expect(cache_service).to receive(:refresh)
+ end
- before do
- project.add_developer(user)
+ service.execute
end
- it 'creates a new protected branch if we skip authorization step' do
- expect { service.execute(skip_authorization: true) }.to change(ProtectedBranch, :count).by(1)
+ context 'when protecting a branch with a name that contains HTML tags' do
+ let(:name) { 'foo<b>bar<\b>' }
+
+ it 'creates a new protected branch' do
+ expect { service.execute }.to change(ProtectedBranch, :count).by(1)
+ expect(entity.protected_branches.last.name).to eq(name)
+ end
end
- it 'raises Gitlab::Access:AccessDeniedError' do
- expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ context 'when a policy restricts rule creation' do
+ it "prevents creation of the protected branch rule" do
+ disallow(:create_protected_branch, an_instance_of(ProtectedBranch))
+
+ expect do
+ service.execute
+ end.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+
+ it 'creates a new protected branch if we skip authorization step' do
+ expect { service.execute(skip_authorization: true) }.to change(ProtectedBranch, :count).by(1)
+ end
end
end
+ end
- context 'when a policy restricts rule creation' do
- it "prevents creation of the protected branch rule" do
- disallow(:create_protected_branch, an_instance_of(ProtectedBranch))
+ context 'with entity project' do
+ let_it_be_with_reload(:entity) { create(:project) }
+ let(:user) { entity.first_owner }
- expect do
- service.execute
- end.to raise_error(Gitlab::Access::AccessDeniedError)
- end
+ it_behaves_like 'execute with entity'
+ end
+
+ context 'with entity group' do
+ let_it_be_with_reload(:entity) { create(:group) }
+ let_it_be_with_reload(:user) { create(:user) }
+
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :create_protected_branch, instance_of(ProtectedBranch)).and_return(true)
end
+
+ it_behaves_like 'execute with entity'
end
def disallow(ability, protected_branch)
diff --git a/spec/services/protected_branches/destroy_service_spec.rb b/spec/services/protected_branches/destroy_service_spec.rb
index 123deeea005..421d4aae5bb 100644
--- a/spec/services/protected_branches/destroy_service_spec.rb
+++ b/spec/services/protected_branches/destroy_service_spec.rb
@@ -3,37 +3,54 @@
require 'spec_helper'
RSpec.describe ProtectedBranches::DestroyService do
- let_it_be_with_reload(:project) { create(:project) }
+ shared_examples 'execute with entity' do
+ subject(:service) { described_class.new(entity, user) }
- let!(:protected_branch) { create(:protected_branch, project: project) }
- let(:user) { project.first_owner }
+ describe '#execute' do
+ it 'destroys a protected branch' do
+ service.execute(protected_branch)
- subject(:service) { described_class.new(project, user) }
-
- describe '#execute' do
- it 'destroys a protected branch' do
- service.execute(protected_branch)
+ expect(protected_branch).to be_destroyed
+ end
- expect(protected_branch).to be_destroyed
- end
+ it 'refreshes the cache' do
+ expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service|
+ expect(cache_service).to receive(:refresh)
+ end
- it 'refreshes the cache' do
- expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service|
- expect(cache_service).to receive(:refresh)
+ service.execute(protected_branch)
end
- service.execute(protected_branch)
+ context 'when a policy restricts rule deletion' do
+ it "prevents deletion of the protected branch rule" do
+ disallow(:destroy_protected_branch, protected_branch)
+
+ expect do
+ service.execute(protected_branch)
+ end.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
end
+ end
- context 'when a policy restricts rule deletion' do
- it "prevents deletion of the protected branch rule" do
- disallow(:destroy_protected_branch, protected_branch)
+ context 'with entity project' do
+ let_it_be_with_reload(:entity) { create(:project) }
+ let!(:protected_branch) { create(:protected_branch, project: entity) }
+ let(:user) { entity.first_owner }
- expect do
- service.execute(protected_branch)
- end.to raise_error(Gitlab::Access::AccessDeniedError)
- end
+ it_behaves_like 'execute with entity'
+ end
+
+ context 'with entity group' do
+ let_it_be_with_reload(:entity) { create(:group) }
+ let_it_be_with_reload(:user) { create(:user) }
+ let!(:protected_branch) { create(:protected_branch, group: entity, project: nil) }
+
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :destroy_protected_branch, protected_branch).and_return(true)
end
+
+ it_behaves_like 'execute with entity'
end
def disallow(ability, protected_branch)
diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb
index 2ff6c3c489a..c70cc032a6a 100644
--- a/spec/services/protected_branches/update_service_spec.rb
+++ b/spec/services/protected_branches/update_service_spec.rb
@@ -3,54 +3,64 @@
require 'spec_helper'
RSpec.describe ProtectedBranches::UpdateService do
- let_it_be_with_reload(:project) { create(:project) }
+ shared_examples 'execute with entity' do
+ let(:params) { { name: new_name } }
- let!(:protected_branch) { create(:protected_branch, project: project) }
- let(:user) { project.first_owner }
- let(:params) { { name: new_name } }
+ subject(:service) { described_class.new(entity, user, params) }
- subject(:service) { described_class.new(project, user, params) }
+ describe '#execute' do
+ let(:new_name) { 'new protected branch name' }
+ let(:result) { service.execute(protected_branch) }
- describe '#execute' do
- let(:new_name) { 'new protected branch name' }
- let(:result) { service.execute(protected_branch) }
+ it 'updates a protected branch' do
+ expect(result.reload.name).to eq(params[:name])
+ end
- it 'updates a protected branch' do
- expect(result.reload.name).to eq(params[:name])
- end
+ it 'refreshes the cache' do
+ expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service|
+ expect(cache_service).to receive(:refresh)
+ end
- it 'refreshes the cache' do
- expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service|
- expect(cache_service).to receive(:refresh)
+ result
end
- result
- end
-
- context 'when updating name of a protected branch to one that contains HTML tags' do
- let(:new_name) { 'foo<b>bar<\b>' }
- let(:result) { service.execute(protected_branch) }
+ context 'when updating name of a protected branch to one that contains HTML tags' do
+ let(:new_name) { 'foo<b>bar<\b>' }
+ let(:result) { service.execute(protected_branch) }
- it 'updates a protected branch' do
- expect(result.reload.name).to eq(new_name)
+ it 'updates a protected branch' do
+ expect(result.reload.name).to eq(new_name)
+ end
end
- end
- context 'without admin_project permissions' do
- let(:user) { create(:user) }
+ context 'when a policy restricts rule update' do
+ it "prevents update of the protected branch rule" do
+ disallow(:update_protected_branch, protected_branch)
- it "raises error" do
- expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
end
end
+ end
- context 'when a policy restricts rule update' do
- it "prevents update of the protected branch rule" do
- disallow(:update_protected_branch, protected_branch)
+ context 'with entity project' do
+ let_it_be_with_reload(:entity) { create(:project) }
+ let!(:protected_branch) { create(:protected_branch, project: entity) }
+ let(:user) { entity.first_owner }
- expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
- end
+ it_behaves_like 'execute with entity'
+ end
+
+ context 'with entity group' do
+ let_it_be_with_reload(:entity) { create(:group) }
+ let_it_be_with_reload(:user) { create(:user) }
+ let!(:protected_branch) { create(:protected_branch, group: entity, project: nil) }
+
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :update_protected_branch, protected_branch).and_return(true)
end
+
+ it_behaves_like 'execute with entity'
end
def disallow(ability, protected_branch)
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index f9c16c84121..8eccb9e41bb 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -567,7 +567,13 @@ RSpec.describe QuickActions::InterpretService do
it 'returns the duplicate message' do
_, _, message = service.execute(content, issuable)
- expect(message).to eq("Marked this issue as a duplicate of #{issue_duplicate.to_reference(project)}.")
+ expect(message).to eq("Closed this issue. Marked as related to, and a duplicate of, #{issue_duplicate.to_reference(project)}.")
+ end
+
+ it 'includes duplicate reference' do
+ _, explanations = service.explain(content, issuable)
+
+ expect(explanations).to eq(["Closes this issue. Marks as related to, and a duplicate of, #{issue_duplicate.to_reference(project)}."])
end
end
@@ -2491,6 +2497,16 @@ RSpec.describe QuickActions::InterpretService do
expect(message).to eq('One or more contacts were successfully removed.')
end
end
+
+ context 'when using an alias' do
+ it 'returns the correct execution message' do
+ content = "/labels ~#{bug.title}"
+
+ _, _, message = service.execute(content, issue)
+
+ expect(message).to eq("Added ~\"Bug\" label.")
+ end
+ end
end
describe '#explain' do
diff --git a/spec/services/repositories/housekeeping_service_spec.rb b/spec/services/repositories/housekeeping_service_spec.rb
index fbd9affb33c..57245136fbe 100644
--- a/spec/services/repositories/housekeeping_service_spec.rb
+++ b/spec/services/repositories/housekeeping_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Repositories::HousekeepingService do
+RSpec.describe Repositories::HousekeepingService, feature_category: :source_code_management do
it_behaves_like 'housekeeps repository' do
let_it_be(:resource) { create(:project, :repository) }
end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 26def474b88..90e80a45515 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SearchService do
+RSpec.describe SearchService, feature_category: :global_search do
let_it_be(:user) { create(:user) }
let_it_be(:accessible_group) { create(:group, :private) }
diff --git a/spec/services/security/merge_reports_service_spec.rb b/spec/services/security/merge_reports_service_spec.rb
index 8415ed8a22f..249f4da5f34 100644
--- a/spec/services/security/merge_reports_service_spec.rb
+++ b/spec/services/security/merge_reports_service_spec.rb
@@ -187,25 +187,25 @@ RSpec.describe Security::MergeReportsService, '#execute' do
it 'deduplicates (except cwe and wasc) and sorts the vulnerabilities by severity (desc) then by compare key' do
expect(merged_report.findings).to(
eq([
- finding_cwe_2,
- finding_wasc_2,
- finding_cwe_1,
- finding_id_2_loc_2,
- finding_id_2_loc_1,
- finding_wasc_1,
- finding_id_1
- ])
+ finding_cwe_2,
+ finding_wasc_2,
+ finding_cwe_1,
+ finding_id_2_loc_2,
+ finding_id_2_loc_1,
+ finding_wasc_1,
+ finding_id_1
+ ])
)
end
it 'deduplicates scanned resources' do
expect(merged_report.scanned_resources).to(
eq([
- scanned_resource,
- scanned_resource_1,
- scanned_resource_2,
- scanned_resource_3
- ])
+ scanned_resource,
+ scanned_resource_1,
+ scanned_resource_2,
+ scanned_resource_3
+ ])
)
end
diff --git a/spec/services/service_ping/submit_service_ping_service_spec.rb b/spec/services/service_ping/submit_service_ping_service_spec.rb
index 5dbf5edb776..37231307156 100644
--- a/spec/services/service_ping/submit_service_ping_service_spec.rb
+++ b/spec/services/service_ping/submit_service_ping_service_spec.rb
@@ -340,7 +340,7 @@ RSpec.describe ServicePing::SubmitService do
end
end
- def stub_response(url: service_ping_payload_url, body:, status: 201)
+ def stub_response(body:, url: service_ping_payload_url, status: 201)
stub_full_request(url, method: :post)
.to_return(
headers: { 'Content-Type' => 'application/json' },
diff --git a/spec/services/timelogs/create_service_spec.rb b/spec/services/timelogs/create_service_spec.rb
index b5ed4a005c7..73860619bcc 100644
--- a/spec/services/timelogs/create_service_spec.rb
+++ b/spec/services/timelogs/create_service_spec.rb
@@ -2,16 +2,12 @@
require 'spec_helper'
-RSpec.describe Timelogs::CreateService do
+RSpec.describe Timelogs::CreateService, feature_category: :team_planning do
let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :public) }
- let_it_be(:time_spent) { 3600 }
- let_it_be(:spent_at) { "2022-07-08" }
- let_it_be(:summary) { "Test summary" }
let(:issuable) { nil }
let(:users_container) { project }
- let(:service) { described_class.new(issuable, time_spent, spent_at, summary, user) }
describe '#execute' do
subject { service.execute }
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 774a6ddcfb3..c4ed34a693e 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -209,6 +209,15 @@ RSpec.describe TodoService do
it_behaves_like 'an incident management tracked event', :incident_management_incident_todo do
let(:current_user) { john_doe }
end
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:namespace) { project.namespace }
+ let(:category) { described_class.to_s }
+ let(:action) { 'incident_management_incident_todo' }
+ let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
+ let(:user) { john_doe }
+ end
end
end
@@ -1250,6 +1259,96 @@ RSpec.describe TodoService do
end
end
+ describe '#create_member_access_request' do
+ context 'snowplow event tracking' do
+ it 'does not track snowplow event when todos are for access request for project', :snowplow do
+ user = create(:user)
+ project = create(:project)
+ requester = create(:project_member, project: project, user: assignee)
+ project.add_owner(user)
+
+ expect_no_snowplow_event
+
+ service.create_member_access_request(requester)
+ end
+ end
+
+ context 'when the group has more than 10 owners' do
+ it 'creates todos for 10 recently active group owners' do
+ group = create(:group, :public)
+
+ users = create_list(:user, 12, :with_sign_ins)
+ users.each do |user|
+ group.add_owner(user)
+ end
+ ten_most_recently_active_group_owners = users.sort_by(&:last_sign_in_at).last(10)
+ excluded_group_owners = users - ten_most_recently_active_group_owners
+
+ requester = create(:group_member, group: group, user: assignee)
+
+ service.create_member_access_request(requester)
+
+ ten_most_recently_active_group_owners.each do |owner|
+ expect(Todo.where(user: owner, target: group, action: Todo::MEMBER_ACCESS_REQUESTED, author: assignee).count).to eq 1
+ end
+
+ excluded_group_owners.each do |owner|
+ expect(Todo.where(user: owner, target: group, action: Todo::MEMBER_ACCESS_REQUESTED, author: assignee).count).to eq 0
+ end
+ end
+ end
+
+ context 'when total owners are less than 10' do
+ it 'creates todos for all group owners' do
+ group = create(:group, :public)
+
+ users = create_list(:user, 4, :with_sign_ins)
+ users.map do |user|
+ group.add_owner(user)
+ end
+
+ requester = create(:group_member, user: assignee, group: group)
+ requester.requested_at = Time.now.utc
+ requester.save!
+
+ service.create_member_access_request(requester)
+
+ users.each do |owner|
+ expect(Todo.where(user: owner, target: group, action: Todo::MEMBER_ACCESS_REQUESTED, author: assignee).count).to eq 1
+ end
+ end
+ end
+
+ context 'when multiple access requests are raised' do
+ it 'creates todos for 10 recently active group owners for multiple requests' do
+ group = create(:group, :public)
+
+ users = create_list(:user, 12, :with_sign_ins)
+ users.each do |user|
+ group.add_owner(user)
+ end
+ ten_most_recently_active_group_owners = users.sort_by(&:last_sign_in_at).last(10)
+ excluded_group_owners = users - ten_most_recently_active_group_owners
+
+ requester1 = create(:group_member, group: group, user: assignee)
+ requester2 = create(:group_member, group: group, user: non_member)
+
+ service.create_member_access_request(requester1)
+ service.create_member_access_request(requester2)
+
+ ten_most_recently_active_group_owners.each do |owner|
+ expect(Todo.where(user: owner, target: group, action: Todo::MEMBER_ACCESS_REQUESTED, author: assignee).count).to eq 1
+ expect(Todo.where(user: owner, target: group, action: Todo::MEMBER_ACCESS_REQUESTED, author: non_member).count).to eq 1
+ end
+
+ excluded_group_owners.each do |owner|
+ expect(Todo.where(user: owner, target: group, action: Todo::MEMBER_ACCESS_REQUESTED, author: assignee).count).to eq 0
+ expect(Todo.where(user: owner, target: group, action: Todo::MEMBER_ACCESS_REQUESTED, author: non_member).count).to eq 0
+ end
+ end
+ end
+ end
+
def should_create_todo(attributes = {})
attributes.reverse_merge!(
project: project,
diff --git a/spec/services/users/assigned_issues_count_service_spec.rb b/spec/services/users/assigned_issues_count_service_spec.rb
new file mode 100644
index 00000000000..afa6a0af3dd
--- /dev/null
+++ b/spec/services/users/assigned_issues_count_service_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::AssignedIssuesCountService, :use_clean_rails_memory_store_caching,
+ feature_category: :project_management do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:max_limit) { 10 }
+
+ let(:current_user) { user }
+
+ subject { described_class.new(current_user: current_user, max_limit: max_limit) }
+
+ it_behaves_like 'a counter caching service'
+
+ context 'when user has assigned open issues from archived and closed projects' do
+ before do
+ project = create(:project, :public)
+ archived_project = create(:project, :public, :archived)
+
+ create(:issue, project: project, author: user, assignees: [user])
+ create(:issue, :closed, project: project, author: user, assignees: [user])
+ create(:issue, project: archived_project, author: user, assignees: [user])
+ end
+
+ it 'count all assigned open issues excluding those from closed or archived projects' do
+ expect(subject.count).to eq(1)
+ end
+ end
+
+ context 'when the number of assigned open issues exceeds max_limit' do
+ let_it_be(:banned_user) { create(:user, :banned) }
+ let_it_be(:project) { create(:project).tap { |p| p.add_developer(user) } }
+
+ context 'when user is admin', :enable_admin_mode do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:issues) { create_list(:issue, max_limit + 1, project: project, assignees: [admin]) }
+ let_it_be(:banned_issue) { create(:issue, project: project, assignees: [admin], author: banned_user) }
+
+ let(:current_user) { admin }
+
+ it 'returns the max_limit count' do
+ expect(subject.count).to eq max_limit
+ end
+ end
+
+ context 'when user is non-admin' do
+ let_it_be(:issues) { create_list(:issue, max_limit + 1, project: project, assignees: [user]) }
+ let_it_be(:closed_issue) { create(:issue, :closed, project: project, assignees: [user]) }
+ let_it_be(:banned_issue) { create(:issue, project: project, assignees: [user], author: banned_user) }
+
+ it 'returns the max_limit count' do
+ expect(subject.count).to eq max_limit
+ end
+ end
+ end
+end
diff --git a/spec/services/users/keys_count_service_spec.rb b/spec/services/users/keys_count_service_spec.rb
index aff267cce5e..607d2946b2c 100644
--- a/spec/services/users/keys_count_service_spec.rb
+++ b/spec/services/users/keys_count_service_spec.rb
@@ -17,6 +17,12 @@ RSpec.describe Users::KeysCountService, :use_clean_rails_memory_store_caching do
it 'returns the number of SSH keys as an Integer' do
expect(subject.count).to eq(1)
end
+
+ it 'does not count signing keys' do
+ create(:key, usage_type: :signing, user: user)
+
+ expect(subject.count).to eq(1)
+ end
end
describe '#uncached_count' do
diff --git a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
index 6082c7bd10e..827d6f652a4 100644
--- a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Users::MigrateRecordsToGhostUserService do
+ include BatchDestroyDependentAssociationsHelper
+
let!(:user) { create(:user) }
let(:service) { described_class.new(user, admin, execution_tracker) }
let(:execution_tracker) { instance_double(::Gitlab::Utils::ExecutionTracker, over_limit?: false) }
@@ -125,6 +127,12 @@ RSpec.describe Users::MigrateRecordsToGhostUserService do
let(:created_record) { create(:review, author: user) }
end
end
+
+ context 'for releases' do
+ include_examples 'migrating records to the ghost user', Release, [:author] do
+ let(:created_record) { create(:release, author: user) }
+ end
+ end
end
context 'on post-migrate cleanups' do
@@ -150,12 +158,6 @@ RSpec.describe Users::MigrateRecordsToGhostUserService do
def nullify_in_batches_regexp(table, column, user, batch_size: 100)
%r{^UPDATE "#{table}" SET "#{column}" = NULL WHERE "#{table}"."id" IN \(SELECT "#{table}"."id" FROM "#{table}" WHERE "#{table}"."#{column}" = #{user.id} LIMIT #{batch_size}\)}
end
-
- def delete_in_batches_regexps(table, column, user, items, batch_size: 1000)
- select_query = %r{^SELECT "#{table}".* FROM "#{table}" WHERE "#{table}"."#{column}" = #{user.id}.*ORDER BY "#{table}"."id" ASC LIMIT #{batch_size}}
-
- [select_query] + items.map { |item| %r{^DELETE FROM "#{table}" WHERE "#{table}"."id" = #{item.id}} }
- end
# rubocop:enable Layout/LineLength
it 'nullifies related associations in batches' do
diff --git a/spec/services/users/registrations_build_service_spec.rb b/spec/services/users/registrations_build_service_spec.rb
index bc3718dbdb2..fa53a4cc604 100644
--- a/spec/services/users/registrations_build_service_spec.rb
+++ b/spec/services/users/registrations_build_service_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Users::RegistrationsBuildService do
context 'when automatic user confirmation is not enabled' do
before do
- stub_application_setting(send_user_confirmation_email: true)
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
end
context 'when skip_confirmation is true' do
@@ -44,7 +44,7 @@ RSpec.describe Users::RegistrationsBuildService do
context 'when automatic user confirmation is enabled' do
before do
- stub_application_setting(send_user_confirmation_email: false)
+ stub_application_setting_enum('email_confirmation_setting', 'off')
end
context 'when skip_confirmation is true' do
diff --git a/spec/services/web_hooks/log_execution_service_spec.rb b/spec/services/web_hooks/log_execution_service_spec.rb
index fd97d01fa9f..8a845f60ad2 100644
--- a/spec/services/web_hooks/log_execution_service_spec.rb
+++ b/spec/services/web_hooks/log_execution_service_spec.rb
@@ -42,14 +42,6 @@ RSpec.describe WebHooks::LogExecutionService do
service.execute
end
- it 'does not update the last failure when the feature flag is disabled' do
- stub_feature_flags(web_hooks_disable_failed: false)
-
- expect(project_hook).not_to receive(:update_last_failure)
-
- service.execute
- end
-
context 'obtaining an exclusive lease' do
let(:lease_key) { "web_hooks:update_hook_failure_state:#{project_hook.id}" }
@@ -136,19 +128,6 @@ RSpec.describe WebHooks::LogExecutionService do
expect { service.execute }.not_to change(project_hook, :recent_failures)
end
-
- context 'when the web_hooks_disable_failed FF is disabled' do
- before do
- # Hook will only be executed if the flag is disabled.
- stub_feature_flags(web_hooks_disable_failed: false)
- end
-
- it 'does not allow the failure count to overflow' do
- project_hook.update!(recent_failures: 32767)
-
- expect { service.execute }.not_to change(project_hook, :recent_failures)
- end
- end
end
context 'when response_category is :error' do
@@ -165,6 +144,24 @@ RSpec.describe WebHooks::LogExecutionService do
end
end
+ context 'with url_variables' do
+ before do
+ project_hook.update!(
+ url: 'http://example1.test/{foo}-{bar}',
+ url_variables: { 'foo' => 'supers3cret', 'bar' => 'token' }
+ )
+ end
+
+ let(:data) { super().merge(response_headers: { 'X-Token-Id' => 'supers3cret-token', 'X-Request' => 'PUBLIC-token' }) }
+ let(:expected_headers) { { 'X-Token-Id' => '{foo}-{bar}', 'X-Request' => 'PUBLIC-{bar}' } }
+
+ it 'logs the data and masks response headers' do
+ expect { service.execute }.to change(::WebHookLog, :count).by(1)
+
+ expect(WebHookLog.recent.first.response_headers).to eq(expected_headers)
+ end
+ end
+
context 'with X-Gitlab-Token' do
let(:request_headers) { { 'X-Gitlab-Token' => project_hook.token } }
diff --git a/spec/services/work_items/create_and_link_service_spec.rb b/spec/services/work_items/create_and_link_service_spec.rb
index e259a22d388..00372d460e1 100644
--- a/spec/services/work_items/create_and_link_service_spec.rb
+++ b/spec/services/work_items/create_and_link_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::CreateAndLinkService do
+RSpec.describe WorkItems::CreateAndLinkService, feature_category: :portfolio_management do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
@@ -94,7 +94,7 @@ RSpec.describe WorkItems::CreateAndLinkService do
end
it 'returns a link creation error message' do
- expect(service_result.errors).to contain_exactly(/only Issue and Incident can be parent of Task./)
+ expect(service_result.errors).to contain_exactly(/is not allowed to add this type of parent/)
end
end
end
diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb
index 1bd7e15db67..a952486ee64 100644
--- a/spec/services/work_items/create_service_spec.rb
+++ b/spec/services/work_items/create_service_spec.rb
@@ -159,7 +159,7 @@ RSpec.describe WorkItems::CreateService do
{
title: 'Awesome work_item',
description: 'please fix',
- work_item_type: create(:work_item_type, :task)
+ work_item_type: WorkItems::Type.default_by_type(:task)
}
end
@@ -176,7 +176,7 @@ RSpec.describe WorkItems::CreateService do
let_it_be(:parent) { create(:work_item, :task, project: project) }
it_behaves_like 'fails creating work item and returns errors' do
- let(:error_message) { 'only Issue and Incident can be parent of Task.' }
+ let(:error_message) { 'is not allowed to add this type of parent' }
end
end
end
diff --git a/spec/services/work_items/parent_links/create_service_spec.rb b/spec/services/work_items/parent_links/create_service_spec.rb
index 0ba41373544..2f2e830845a 100644
--- a/spec/services/work_items/parent_links/create_service_spec.rb
+++ b/spec/services/work_items/parent_links/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::ParentLinks::CreateService do
+RSpec.describe WorkItems::ParentLinks::CreateService, feature_category: :portfolio_management do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:guest) { create(:user) }
@@ -117,7 +117,7 @@ RSpec.describe WorkItems::ParentLinks::CreateService do
end
it 'returns error status' do
- error = "#{issue.to_reference} cannot be added: only Task can be assigned as a child in hierarchy.. " \
+ error = "#{issue.to_reference} cannot be added: is not allowed to add this type of parent. " \
"#{other_project_task.to_reference} cannot be added: parent must be in the same project as child."
is_expected.to eq(service_error(error, http_status: 422))
@@ -139,7 +139,7 @@ RSpec.describe WorkItems::ParentLinks::CreateService do
let(:params) { { target_issuable: task1 } }
it 'returns error status' do
- error = "#{task1.to_reference} cannot be added: only Issue and Incident can be parent of Task."
+ error = "#{task1.to_reference} cannot be added: is not allowed to add this type of parent"
is_expected.to eq(service_error(error, http_status: 422))
end
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index 68efb4c220b..87665bcad2c 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -284,7 +284,7 @@ RSpec.describe WorkItems::UpdateService do
it 'returns error status' do
expect(subject[:status]).to be(:error)
expect(subject[:message])
- .to match("#{child_work_item.to_reference} cannot be added: only Task can be assigned as a child in hierarchy.")
+ .to match("#{child_work_item.to_reference} cannot be added: is not allowed to add this type of parent")
end
it 'does not update work item attributes' do
diff --git a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb
index 1b8c4c5f15f..5a5bb8a1674 100644
--- a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb
+++ b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
+RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService, feature_category: :portfolio_management do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
@@ -81,7 +81,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
it_behaves_like 'raises a WidgetError' do
let(:message) do
- "#{child_issue.to_reference} cannot be added: only Task can be assigned as a child in hierarchy."
+ "#{child_issue.to_reference} cannot be added: is not allowed to add this type of parent"
end
end
end
@@ -136,7 +136,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
it_behaves_like 'raises a WidgetError' do
let(:message) do
- "#{work_item.to_reference} cannot be added: only Issue and Incident can be parent of Task."
+ "#{work_item.to_reference} cannot be added: is not allowed to add this type of parent"
end
end
end
diff --git a/spec/sidekiq_cluster/sidekiq_cluster_spec.rb b/spec/sidekiq_cluster/sidekiq_cluster_spec.rb
index c0a919a4aec..25a600405fe 100644
--- a/spec/sidekiq_cluster/sidekiq_cluster_spec.rb
+++ b/spec/sidekiq_cluster/sidekiq_cluster_spec.rb
@@ -20,13 +20,16 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
"SIDEKIQ_WORKER_ID" => "0"
},
"bundle", "exec", "sidekiq", "-c10", "-eproduction", "-t25", "-gqueues:foo", "-rfoo/bar", "-qfoo,1", process_options
- )
+ ).and_return(1)
+ expect(Process).to receive(:detach).ordered.with(1)
+
expect(Process).to receive(:spawn).ordered.with({
"ENABLE_SIDEKIQ_CLUSTER" => "1",
"SIDEKIQ_WORKER_ID" => "1"
},
"bundle", "exec", "sidekiq", "-c10", "-eproduction", "-t25", "-gqueues:bar,baz", "-rfoo/bar", "-qbar,1", "-qbaz,1", process_options
- )
+ ).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)
end
@@ -35,7 +38,7 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
expected_options = {
env: :development,
directory: an_instance_of(String),
- max_concurrency: 50,
+ max_concurrency: 20,
min_concurrency: 0,
worker_id: an_instance_of(Integer),
timeout: 25,
@@ -58,11 +61,13 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
let(:env) { { "ENABLE_SIDEKIQ_CLUSTER" => "1", "SIDEKIQ_WORKER_ID" => first_worker_id.to_s } }
let(:args) { ['bundle', 'exec', 'sidekiq', anything, '-eproduction', '-t10', *([anything] * 5)] }
+ let(:waiter_thread) { instance_double('Process::Waiter') }
+
it 'starts a Sidekiq process' do
allow(Process).to receive(:spawn).and_return(1)
+ allow(Process).to receive(:detach).with(1).and_return(waiter_thread)
- expect(Gitlab::ProcessManagement).to receive(:wait_async).with(1)
- expect(described_class.start_sidekiq(%w(foo), **options)).to eq(1)
+ expect(described_class.start_sidekiq(%w(foo), **options)).to eq(waiter_thread)
end
it 'handles duplicate queue names' do
@@ -70,9 +75,9 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
.to receive(:spawn)
.with(env, *args, anything)
.and_return(1)
+ allow(Process).to receive(:detach).with(1).and_return(waiter_thread)
- expect(Gitlab::ProcessManagement).to receive(:wait_async).with(1)
- expect(described_class.start_sidekiq(%w(foo foo bar baz), **options)).to eq(1)
+ 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
@@ -80,9 +85,9 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
.to receive(:spawn)
.with(anything, *args, a_hash_including(pgroup: true))
.and_return(1)
+ allow(Process).to receive(:detach).with(1).and_return(waiter_thread)
- allow(Gitlab::ProcessManagement).to receive(:wait_async)
- expect(described_class.start_sidekiq(%w(foo bar baz), **options)).to eq(1)
+ expect(described_class.start_sidekiq(%w(foo bar baz), **options)).to eq(waiter_thread)
end
end
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
index dbaecc6a233..70bd01091ba 100644
--- a/spec/simplecov_env.rb
+++ b/spec/simplecov_env.rb
@@ -21,12 +21,14 @@ module SimpleCovEnv
def configure_formatter
SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
- SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([
- SimpleCov::Formatter::SimpleFormatter,
- SimpleCov::Formatter::HTMLFormatter,
- SimpleCov::Formatter::CoberturaFormatter,
- SimpleCov::Formatter::LcovFormatter
- ])
+ SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new(
+ [
+ SimpleCov::Formatter::SimpleFormatter,
+ SimpleCov::Formatter::HTMLFormatter,
+ SimpleCov::Formatter::CoberturaFormatter,
+ SimpleCov::Formatter::LcovFormatter
+ ]
+ )
end
def configure_job
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 8e73073e68b..23083203cfe 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -211,7 +211,6 @@ RSpec.configure do |config|
end
config.before(:suite) do
- Timecop.safe_mode = true
TestEnv.init
# Reload all feature flags definitions
@@ -271,6 +270,10 @@ RSpec.configure do |config|
# cause spec failures.
stub_feature_flags(use_click_house_database_for_error_tracking: false)
+ # Disable this to avoid the Web IDE modals popping up in tests:
+ # 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
diff --git a/spec/support/atlassian/jira_connect/schemata.rb b/spec/support/atlassian/jira_connect/schemata.rb
index 61e8aa8e15c..73a6833b7cc 100644
--- a/spec/support/atlassian/jira_connect/schemata.rb
+++ b/spec/support/atlassian/jira_connect/schemata.rb
@@ -11,7 +11,7 @@ module Atlassian
schemaVersion pipelineId buildNumber updateSequenceNumber
displayName url state issueKeys testInfo references
lastUpdated
- ),
+ ),
'properties' => {
'schemaVersion' => schema_version_type,
'pipelineId' => { 'type' => 'string' },
diff --git a/spec/support/banzai/filter_timeout_shared_examples.rb b/spec/support/banzai/filter_timeout_shared_examples.rb
new file mode 100644
index 00000000000..1f2ebe6fef6
--- /dev/null
+++ b/spec/support/banzai/filter_timeout_shared_examples.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+# This shared_example requires the following variables:
+# - text: The text to be run through the filter
+#
+# Usage:
+#
+# it_behaves_like 'filter timeout' do
+# let(:text) { 'some text' }
+# end
+RSpec.shared_examples 'filter timeout' do
+ context 'when rendering takes too long' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:context) { { project: project } }
+
+ it 'times out' do
+ stub_const("Banzai::Filter::TimeoutHtmlPipelineFilter::RENDER_TIMEOUT", 0.1)
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:call_with_timeout) do
+ sleep(0.2)
+ text
+ end
+ end
+
+ expect(Gitlab::RenderTimeout).to receive(:timeout).and_call_original
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(Timeout::Error),
+ project_id: context[:project].id,
+ class_name: described_class.name.demodulize
+ )
+
+ result = filter(text)
+
+ expect(result.to_html).to eq text
+ end
+ end
+end
diff --git a/spec/support/before_all_adapter.rb b/spec/support/before_all_adapter.rb
index 890bdd6a2c4..f4946ff271f 100644
--- a/spec/support/before_all_adapter.rb
+++ b/spec/support/before_all_adapter.rb
@@ -1,27 +1,44 @@
# frozen_string_literal: true
-class BeforeAllAdapter # rubocop:disable Gitlab/NamespacedClass
- def self.all_connection_classes
- @all_connection_classes ||= [ActiveRecord::Base] + ActiveRecord::Base.descendants.select(&:connection_class?) # rubocop: disable Database/MultipleDatabases
- end
-
- def self.begin_transaction
- self.all_connection_classes.each do |connection_class|
- connection_class.connection.begin_transaction(joinable: false)
+module TestProfBeforeAllAdapter
+ module MultipleDatabaseAdapter
+ def self.all_connection_classes
+ @all_connection_classes ||= [ActiveRecord::Base] + ActiveRecord::Base.descendants.select(&:connection_class?) # rubocop: disable Database/MultipleDatabases
end
- end
- def self.rollback_transaction
- self.all_connection_classes.each do |connection_class|
- if connection_class.connection.open_transactions.zero?
- warn "!!! before_all transaction has been already rollbacked and " \
- "could work incorrectly"
- next
+ def self.begin_transaction
+ self.all_connection_classes.each do |connection_class|
+ connection_class.connection.begin_transaction(joinable: false)
end
+ end
- connection_class.connection.rollback_transaction
+ def self.rollback_transaction
+ self.all_connection_classes.each do |connection_class|
+ if connection_class.connection.open_transactions.zero?
+ warn "!!! before_all transaction has been already rollbacked and " \
+ "could work incorrectly"
+ next
+ end
+
+ connection_class.connection.rollback_transaction
+ end
end
end
+
+ # This class is required so we can disable transactions on migration specs
+ module NoTransactionAdapter
+ def self.begin_transaction; end
+
+ def self.rollback_transaction; end
+ end
+
+ def self.default_adapter
+ MultipleDatabaseAdapter
+ end
+
+ def self.no_transaction_adapter
+ NoTransactionAdapter
+ end
end
-TestProf::BeforeAll.adapter = ::BeforeAllAdapter
+TestProf::BeforeAll.adapter = ::TestProfBeforeAllAdapter.default_adapter
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 57065400220..aea853d1c23 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -16,15 +16,17 @@ Capybara.server_port = ENV['CAPYBARA_PORT'] if ENV['CAPYBARA_PORT']
JSConsoleError = Class.new(StandardError)
# Filter out innocuous JS console messages
-JS_CONSOLE_FILTER = Regexp.union([
- '"[HMR] Waiting for update signal from WDS..."',
- '"[WDS] Hot Module Replacement enabled."',
- '"[WDS] Live Reloading enabled."',
- 'Download the Vue Devtools extension',
- 'Download the Apollo DevTools',
- "Unrecognized feature: 'interest-cohort'",
- 'Does this page need fixes or improvements?'
-])
+JS_CONSOLE_FILTER = Regexp.union(
+ [
+ '"[HMR] Waiting for update signal from WDS..."',
+ '"[WDS] Hot Module Replacement enabled."',
+ '"[WDS] Live Reloading enabled."',
+ 'Download the Vue Devtools extension',
+ 'Download the Apollo DevTools',
+ "Unrecognized feature: 'interest-cohort'",
+ 'Does this page need fixes or improvements?'
+ ]
+)
CAPYBARA_WINDOW_SIZE = [1366, 768].freeze
diff --git a/spec/support/counter_attribute.rb b/spec/support/counter_attribute.rb
index 8bd40b72dcf..44df2df0ea5 100644
--- a/spec/support/counter_attribute.rb
+++ b/spec/support/counter_attribute.rb
@@ -7,12 +7,15 @@ RSpec.configure do |config|
CounterAttributeModel.class_eval do
include CounterAttribute
+ after_initialize { self.allow_package_size_counter = true }
+
counter_attribute :build_artifacts_size
counter_attribute :commit_count
+ counter_attribute :packages_size, if: ->(instance) { instance.allow_package_size_counter }
- attr_accessor :flushed
+ attr_accessor :flushed, :allow_package_size_counter
- counter_attribute_after_flush do |subject|
+ counter_attribute_after_commit do |subject|
subject.flushed = true
end
end
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
index f866220b919..816caf5f775 100644
--- a/spec/support/cycle_analytics_helpers/test_generation.rb
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -42,17 +42,17 @@ module CycleAnalyticsHelpers
end_time = start_time + rand(1..5).days
start_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(start_time) { condition_fn[self, data] }
+ travel_to(start_time) { condition_fn[self, data] }
end
# Run `before_end_fn` at the midpoint between `start_time` and `end_time`
- Timecop.freeze(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn
+ travel_to(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn
end_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(end_time) { condition_fn[self, data] }
+ travel_to(end_time) { condition_fn[self, data] }
end
- Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ travel_to(end_time + 1.day) { post_fn[self, data] } if post_fn
end_time - start_time
end
@@ -74,14 +74,14 @@ module CycleAnalyticsHelpers
end_time = rand(1..10).days.from_now
start_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(start_time) { condition_fn[self, data] }
+ travel_to(start_time) { condition_fn[self, data] }
end
end_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(end_time) { condition_fn[self, data] }
+ travel_to(end_time) { condition_fn[self, data] }
end
- Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ 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
@@ -97,17 +97,17 @@ module CycleAnalyticsHelpers
end_time = start_time + rand(1..5).days
# Run `before_end_fn` at the midpoint between `start_time` and `end_time`
- Timecop.freeze(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn
+ travel_to(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn
end_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(start_time) { condition_fn[self, data] }
+ travel_to(start_time) { condition_fn[self, data] }
end
start_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(end_time) { condition_fn[self, data] }
+ travel_to(end_time) { condition_fn[self, data] }
end
- Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ travel_to(end_time + 1.day) { post_fn[self, data] } if post_fn
expect(subject[phase].project_median).to be_nil
end
@@ -122,10 +122,10 @@ module CycleAnalyticsHelpers
end_time = rand(1..10).days.from_now
end_time_conditions.each_with_index do |(_condition_name, condition_fn), index|
- Timecop.freeze(end_time + index.days) { condition_fn[self, data] }
+ travel_to(end_time + index.days) { condition_fn[self, data] }
end
- Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ travel_to(end_time + 1.day) { post_fn[self, data] } if post_fn
expect(subject[phase].project_median).to be_nil
end
@@ -139,7 +139,7 @@ module CycleAnalyticsHelpers
start_time = Time.now
start_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(start_time) { condition_fn[self, data] }
+ travel_to(start_time) { condition_fn[self, data] }
end
post_fn[self, data] if post_fn
diff --git a/spec/support/database/query_recorder.rb b/spec/support/database/query_recorder.rb
index 1050120e528..c0736221af3 100644
--- a/spec/support/database/query_recorder.rb
+++ b/spec/support/database/query_recorder.rb
@@ -3,7 +3,15 @@
RSpec.configure do |config|
# Truncate the query_recorder log file before starting the suite
config.before(:suite) do
- log_path = Rails.root.join(Gitlab::Database::QueryAnalyzers::QueryRecorder::LOG_FILE)
- File.write(log_path, '') if File.exist?(log_path)
+ log_file = Rails.root.join(Gitlab::Database::QueryAnalyzers::QueryRecorder.log_file)
+ File.write(log_file, '') if File.exist?(log_file)
+ File.delete("#{log_file}.gz") if File.exist?("#{log_file}.gz")
+ end
+
+ config.after(:suite) do
+ if ENV['CI']
+ log_file = Rails.root.join(Gitlab::Database::QueryAnalyzers::QueryRecorder.log_file)
+ system("gzip #{log_file}") if File.exist?(log_file)
+ end
end
end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index 24cdbe04fc2..588fe466a42 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -2,7 +2,7 @@
module DbCleaner
def all_connection_classes
- ::BeforeAllAdapter.all_connection_classes
+ ::TestProfBeforeAllAdapter::MultipleDatabaseAdapter.all_connection_classes
end
def delete_from_all_tables!(except: [])
@@ -12,7 +12,7 @@ module DbCleaner
end
def deletion_except_tables
- ['work_item_types']
+ %w[work_item_types work_item_hierarchy_restrictions]
end
def setup_database_cleaner
diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml
index c8af07905c2..750295e16c4 100644
--- a/spec/support/finder_collection_allowlist.yml
+++ b/spec/support/finder_collection_allowlist.yml
@@ -1,8 +1,10 @@
# Allow list for spec/support/finder_collection.rb
-# Permenant excludes
+# Permanent excludes
# For example:
# FooFinder # Reason: It uses a memory backend
+- Namespaces::BilledUsersFinder # Reason: There is no need to have anything else besides the ids is current structure
+- Namespaces::FreeUserCap::UsersFinder # Reason: There is no need to have anything else besides the count
# Temporary excludes (aka TODOs)
# For example:
diff --git a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
index e9a13f7bf63..cef9860fe25 100644
--- a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
+++ b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
@@ -30,7 +30,7 @@ RSpec.shared_examples 'a correct instrumented metric query' do |params|
end
before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(metric.send(:relation).connection).to receive(:transaction_open?).and_return(false)
end
it 'has correct generate query' do
diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml
index 94523591765..dbbfe044e35 100644
--- a/spec/support/gitlab_stubs/gitlab_ci.yml
+++ b/spec/support/gitlab_stubs/gitlab_ci.yml
@@ -12,7 +12,8 @@ variables:
description: 'value of KEY_VALUE_VAR'
DB_NAME: postgres
ENVIRONMENT_VAR:
- value: ['env var value', 'env var value2']
+ value: 'env var value'
+ options: ['env var value', 'env var value2']
description: 'env var description'
stages:
diff --git a/spec/support/helpers/batch_destroy_dependent_associations_helper.rb b/spec/support/helpers/batch_destroy_dependent_associations_helper.rb
new file mode 100644
index 00000000000..22170de053b
--- /dev/null
+++ b/spec/support/helpers/batch_destroy_dependent_associations_helper.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module BatchDestroyDependentAssociationsHelper
+ private
+
+ def delete_in_batches_regexps(table, column, resource, items, batch_size: 1000)
+ # rubocop:disable Layout/LineLength
+ select_query = %r{^SELECT "#{table}".* FROM "#{table}" WHERE.* "#{table}"."#{column}" = #{resource.id}.*ORDER BY "#{table}"."id" ASC LIMIT #{batch_size}}
+ # rubocop:enable Layout/LineLength
+
+ [select_query] + items.map { |item| %r{^DELETE FROM "#{table}" WHERE "#{table}"."id" = #{item.id}} }
+ end
+end
diff --git a/spec/support/helpers/ci/partitioning_helpers.rb b/spec/support/helpers/ci/partitioning_helpers.rb
new file mode 100644
index 00000000000..110199a3147
--- /dev/null
+++ b/spec/support/helpers/ci/partitioning_helpers.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Ci
+ module PartitioningHelpers
+ def stub_current_partition_id(id = Ci::PartitioningTesting::PartitionIdentifiers.ci_testing_partition_id)
+ allow(::Ci::Pipeline)
+ .to receive(:current_partition_value)
+ .and_return(id)
+ end
+ end
+end
diff --git a/spec/support/helpers/content_security_policy_helpers.rb b/spec/support/helpers/content_security_policy_helpers.rb
index 230075ead70..7e3de9fd219 100644
--- a/spec/support/helpers/content_security_policy_helpers.rb
+++ b/spec/support/helpers/content_security_policy_helpers.rb
@@ -4,11 +4,17 @@ module ContentSecurityPolicyHelpers
# Expecting 2 calls to current_content_security_policy by default:
# 1. call that's being tested
# 2. call in ApplicationController
- def setup_csp_for_controller(controller_class, csp = ActionDispatch::ContentSecurityPolicy.new, times: 2)
+ def setup_csp_for_controller(
+ controller_class, csp = ActionDispatch::ContentSecurityPolicy.new, times: 2,
+any_time: false)
expect_next_instance_of(controller_class) do |controller|
- expect(controller)
+ if any_time
+ expect(controller).to receive(:current_content_security_policy).at_least(:once).and_return(csp)
+ else
+ expect(controller)
.to receive(:current_content_security_policy).exactly(times).times
.and_return(csp)
+ end
end
end
end
diff --git a/spec/support/helpers/cookie_helper.rb b/spec/support/helpers/cookie_helper.rb
index ea4be12355b..8971c03a5cc 100644
--- a/spec/support/helpers/cookie_helper.rb
+++ b/spec/support/helpers/cookie_helper.rb
@@ -27,6 +27,12 @@ module CookieHelper
page.driver.browser.manage.cookie_named(name)
end
+ def wait_for_cookie_set(name)
+ wait_for("Complete setting cookie") do
+ get_cookie(name)
+ end
+ end
+
private
def on_a_page?
diff --git a/spec/support/helpers/countries_controller_test_helper.rb b/spec/support/helpers/countries_controller_test_helper.rb
deleted file mode 100644
index 5d36a29bba7..00000000000
--- a/spec/support/helpers/countries_controller_test_helper.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module CountriesControllerTestHelper
- def world_deny_list
- ::World::DENYLIST + ::World::JH_MARKET
- end
-end
-
-CountriesControllerTestHelper.prepend_mod
diff --git a/spec/support/helpers/doc_url_helper.rb b/spec/support/helpers/doc_url_helper.rb
index bbff4827c56..f98c16a3cc4 100644
--- a/spec/support/helpers/doc_url_helper.rb
+++ b/spec/support/helpers/doc_url_helper.rb
@@ -13,7 +13,7 @@ module DocUrlHelper
"#{documentation_base_url}/ee/#{path}.html"
end
- def stub_doc_file_read(file_name: 'index.md', content: )
+ def stub_doc_file_read(content:, file_name: 'index.md')
expect_file_read(File.join(Rails.root, 'doc', file_name), content: content)
end
end
diff --git a/spec/support/helpers/features/branches_helpers.rb b/spec/support/helpers/features/branches_helpers.rb
index 2a50b41cb4e..d4f96718cc0 100644
--- a/spec/support/helpers/features/branches_helpers.rb
+++ b/spec/support/helpers/features/branches_helpers.rb
@@ -22,10 +22,14 @@ module Spec
end
def select_branch(branch_name)
- find(".js-branch-select").click
+ ref_selector = '.ref-selector'
+ find(ref_selector).click
+ wait_for_requests
- page.within("#new-branch-form .dropdown-menu") do
- click_link(branch_name)
+ page.within(ref_selector) do
+ fill_in _('Search by Git revision'), with: branch_name
+ wait_for_requests
+ find('li', text: branch_name, match: :prefer_exact).click
end
end
end
diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb
index d02ec06d886..47cbd6b5208 100644
--- a/spec/support/helpers/features/invite_members_modal_helper.rb
+++ b/spec/support/helpers/features/invite_members_modal_helper.rb
@@ -5,7 +5,7 @@ module Spec
module Helpers
module Features
module InviteMembersModalHelper
- def invite_member(names, role: 'Guest', expires_at: nil, refresh: true)
+ def invite_member(names, role: 'Guest', expires_at: nil)
click_on 'Invite members'
page.within invite_modal_selector do
@@ -14,7 +14,23 @@ module Spec
submit_invites
end
- page.refresh if refresh
+ wait_for_requests
+ end
+
+ def invite_member_by_email(role)
+ click_on _('Invite members')
+
+ page.within invite_modal_selector do
+ choose_options(role, nil)
+ find(member_dropdown_selector).set('new_email@gitlab.com')
+ wait_for_requests
+
+ find('.dropdown-item', text: 'Invite "new_email@gitlab.com" by email').click
+
+ submit_invites
+
+ wait_for_requests
+ end
end
def input_invites(names)
@@ -43,8 +59,6 @@ module Spec
choose_options(role, expires_at)
submit_invites
-
- page.refresh
end
def submit_invites
@@ -52,12 +66,7 @@ module Spec
end
def choose_options(role, expires_at)
- unless role == 'Guest'
- click_button 'Guest'
- wait_for_requests
- click_button role
- end
-
+ select role, from: 'Select a role'
fill_in 'YYYY-MM-DD', with: expires_at.strftime('%Y-%m-%d') if expires_at
end
diff --git a/spec/support/helpers/features/runners_helpers.rb b/spec/support/helpers/features/runners_helpers.rb
index 63fc628358c..c5d26108953 100644
--- a/spec/support/helpers/features/runners_helpers.rb
+++ b/spec/support/helpers/features/runners_helpers.rb
@@ -50,7 +50,7 @@ module Spec
page.within(search_bar_selector) do
click_on filter
- # For OPERATOR_IS_ONLY, clicking the filter
+ # For OPERATORS_IS, clicking the filter
# immediately preselects "=" operator
page.find('input').send_keys(value)
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 278dc79e1d0..20c104cd85c 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -205,7 +205,7 @@ module GitalySetup
# This code needs to work in an environment where we cannot use bundler,
# so we cannot easily use the toml-rb gem. This ad-hoc parser should be
# good enough.
- config_text = IO.read(toml)
+ config_text = File.read(toml)
config_text.lines.each do |line|
match_data = line.match(/^\s*(socket_path|listen_addr)\s*=\s*"([^"]*)"$/)
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index bd0efc96bd8..2176a477371 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -859,7 +859,7 @@ module GraphqlHelpers
# A lookahead that selects everything
def positive_lookahead
- double(selects?: true).tap do |selection|
+ double(selected?: true, selects?: true).tap do |selection|
allow(selection).to receive(:selection).and_return(selection)
allow(selection).to receive(:selections).and_return(selection)
allow(selection).to receive(:map).and_return(double(include?: true))
@@ -868,7 +868,7 @@ module GraphqlHelpers
# A lookahead that selects nothing
def negative_lookahead
- double(selects?: false).tap do |selection|
+ double(selected?: false, selects?: false, selections: []).tap do |selection|
allow(selection).to receive(:selection).and_return(selection)
end
end
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
index 32e6e8d50bd..40eb46878ad 100644
--- a/spec/support/helpers/javascript_fixtures_helpers.rb
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -3,12 +3,14 @@
require 'action_dispatch/testing/test_request'
require 'fileutils'
require 'graphlyte'
+require 'active_support/testing/time_helpers'
require_relative '../../../lib/gitlab/popen'
module JavaScriptFixturesHelpers
extend ActiveSupport::Concern
include Gitlab::Popen
+ include ActiveSupport::Testing::TimeHelpers
extend self
@@ -22,7 +24,7 @@ module JavaScriptFixturesHelpers
# pick an arbitrary date from the past, so tests are not time dependent
# Also see spec/frontend/__helpers__/fake_date/jest.js
- Timecop.freeze(Time.utc(2015, 7, 3, 10)) { example.run }
+ travel_to(Time.utc(2015, 7, 3, 10)) { example.run }
raise NoMethodError.new('You need to set `response` for the fixture generator! This will automatically happen with `type: :controller` or `type: :request`.', 'response') unless respond_to?(:response)
diff --git a/spec/support/helpers/listbox_input_helper.rb b/spec/support/helpers/listbox_input_helper.rb
new file mode 100644
index 00000000000..ca7fbac5daa
--- /dev/null
+++ b/spec/support/helpers/listbox_input_helper.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module ListboxInputHelper
+ include WaitForRequests
+
+ def listbox_input(value, from:)
+ open_listbox_input(from) do
+ find('[role="option"]', text: value).click
+ end
+ end
+
+ def open_listbox_input(selector)
+ page.within(selector) do
+ page.find('button[aria-haspopup="listbox"]').click
+ yield
+ end
+ end
+end
diff --git a/spec/support/helpers/migrations_helpers/work_item_types_helper.rb b/spec/support/helpers/migrations_helpers/work_item_types_helper.rb
index b05caf265ee..40f84486537 100644
--- a/spec/support/helpers/migrations_helpers/work_item_types_helper.rb
+++ b/spec/support/helpers/migrations_helpers/work_item_types_helper.rb
@@ -2,26 +2,9 @@
module MigrationHelpers
module WorkItemTypesHelper
- DEFAULT_WORK_ITEM_TYPES = {
- issue: { name: 'Issue', icon_name: 'issue-type-issue', enum_value: 0 },
- incident: { name: 'Incident', icon_name: 'issue-type-incident', enum_value: 1 },
- test_case: { name: 'Test Case', icon_name: 'issue-type-test-case', enum_value: 2 },
- requirement: { name: 'Requirement', icon_name: 'issue-type-requirements', enum_value: 3 },
- task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 }
- }.freeze
-
def reset_work_item_types
- work_item_types_table.delete_all
-
- DEFAULT_WORK_ITEM_TYPES.each do |type, attributes|
- work_item_types_table.create!(base_type: attributes[:enum_value], **attributes.slice(:name, :icon_name))
- end
- end
-
- private
-
- def work_item_types_table
- table(:work_item_types)
+ Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types
+ Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions
end
end
end
diff --git a/spec/support/helpers/project_template_test_helper.rb b/spec/support/helpers/project_template_test_helper.rb
index eab41f6a1cf..bedbb8601e8 100644
--- a/spec/support/helpers/project_template_test_helper.rb
+++ b/spec/support/helpers/project_template_test_helper.rb
@@ -3,14 +3,14 @@
module ProjectTemplateTestHelper
def all_templates
%w[
- rails spring express iosswift dotnetcore android
- gomicro gatsby hugo jekyll plainhtml gitbook
- hexo middleman gitpod_spring_petclinic nfhugo
- nfjekyll nfplainhtml nfgitbook nfhexo salesforcedx
- serverless_framework tencent_serverless_framework
- jsonnet cluster_management kotlin_native_linux
- pelican
- ]
+ rails spring express iosswift dotnetcore android
+ gomicro gatsby hugo jekyll plainhtml gitbook
+ hexo middleman gitpod_spring_petclinic nfhugo
+ nfjekyll nfplainhtml nfgitbook nfhexo salesforcedx
+ serverless_framework tencent_serverless_framework
+ jsonnet cluster_management kotlin_native_linux
+ pelican bridgetown typo3_distribution
+ ]
end
end
diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
index e76a1dd5a74..9f37cf61cc9 100644
--- a/spec/support/helpers/repo_helpers.rb
+++ b/spec/support/helpers/repo_helpers.rb
@@ -137,4 +137,28 @@ eos
file_content: content
).execute
end
+
+ def create_and_delete_files(project, files, &block)
+ files.each do |filename, content|
+ project.repository.create_file(
+ project.creator,
+ filename,
+ content,
+ message: "Automatically created file #{filename}",
+ branch_name: project.default_branch_or_main
+ )
+ end
+
+ yield
+
+ ensure
+ files.each do |filename, _content|
+ project.repository.delete_file(
+ project.creator,
+ filename,
+ message: "Automatically deleted file #{filename}",
+ branch_name: project.default_branch_or_main
+ )
+ end
+ end
end
diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb
index 7d0f8c09933..eab30be9243 100644
--- a/spec/support/helpers/search_helpers.rb
+++ b/spec/support/helpers/search_helpers.rb
@@ -35,6 +35,8 @@ module SearchHelpers
def select_search_scope(scope)
page.within '[data-testid="search-filter"]' do
click_link scope
+
+ wait_for_all_requests
end
end
diff --git a/spec/support/helpers/service_desk_helper.rb b/spec/support/helpers/service_desk_helper.rb
new file mode 100644
index 00000000000..d67ee5b8a11
--- /dev/null
+++ b/spec/support/helpers/service_desk_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ServiceDeskHelper
+ def set_template_file(file_name, content)
+ file_path = ".gitlab/issue_templates/#{file_name}.md"
+ project.repository.create_file(user, file_path, content, message: 'message', branch_name: 'master')
+ settings.update!(issue_template_key: file_name)
+ end
+end
diff --git a/spec/support/helpers/smime_helper.rb b/spec/support/helpers/smime_helper.rb
index fa16c433c6b..1a414c72fbb 100644
--- a/spec/support/helpers/smime_helper.rb
+++ b/spec/support/helpers/smime_helper.rb
@@ -17,7 +17,7 @@ module SmimeHelper
end
# returns a hash { key:, cert: } containing a generated key, cert pair
- def issue(email_address: 'test@example.com', cn: nil, signed_by:, expires_in:, certificate_authority:)
+ def issue(signed_by:, expires_in:, certificate_authority:, email_address: 'test@example.com', cn: nil)
key = OpenSSL::PKey::RSA.new(4096)
public_key = key.public_key
diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb
index 24c768258a1..4ca8f26be9e 100644
--- a/spec/support/helpers/stub_configuration.rb
+++ b/spec/support/helpers/stub_configuration.rb
@@ -20,6 +20,17 @@ module StubConfiguration
allow_any_instance_of(ApplicationSetting).to receive(:cached_html_up_to_date?).and_return(false)
end
+ # For enums with `_prefix: true`, this allows us to stub the application setting properly
+ def stub_application_setting_enum(setting, value)
+ stub_application_setting(setting.to_sym => value)
+
+ ApplicationSetting.send(setting.pluralize.to_sym).each_key do |key|
+ stub_application_setting("#{setting}_#{key}".to_sym => key == value)
+ end
+
+ Gitlab::CurrentSettings.send(setting)
+ end
+
def stub_not_protect_default_branch
stub_application_setting(
default_branch_protection: Gitlab::Access::PROTECTION_NONE)
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index 87e2a71b1cd..c163ce1d880 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -12,7 +12,6 @@ module StubObjectStorage
uploader:,
enabled: true,
proxy_download: false,
- background_upload: false,
direct_upload: false,
cdn: {}
)
@@ -20,7 +19,6 @@ module StubObjectStorage
new_config = config.to_h.deep_symbolize_keys.merge({
enabled: enabled,
proxy_download: proxy_download,
- background_upload: background_upload,
direct_upload: direct_upload,
cdn: cdn
})
@@ -30,7 +28,6 @@ module StubObjectStorage
allow(config).to receive(:to_h).and_return(new_config)
allow(config).to receive(:enabled) { enabled }
allow(config).to receive(:proxy_download) { proxy_download }
- allow(config).to receive(:background_upload) { background_upload }
allow(config).to receive(:direct_upload) { direct_upload }
uploader_config = Settingslogic.new(new_config.deep_stringify_keys)
diff --git a/spec/support/helpers/stub_snowplow.rb b/spec/support/helpers/stub_snowplow.rb
index 85c605efea3..80342863f7b 100644
--- a/spec/support/helpers/stub_snowplow.rb
+++ b/spec/support/helpers/stub_snowplow.rb
@@ -10,7 +10,7 @@ module StubSnowplow
# rubocop:disable RSpec/AnyInstanceOf
allow_any_instance_of(Gitlab::Tracking::Destinations::Snowplow)
.to receive(:emitter)
- .and_return(SnowplowTracker::Emitter.new(host, buffer_size: buffer_size))
+ .and_return(SnowplowTracker::Emitter.new(endpoint: host, options: { buffer_size: buffer_size }))
# rubocop:enable RSpec/AnyInstanceOf
stub_application_setting(snowplow_enabled: true, snowplow_collector_hostname: host)
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index e1b461cf37e..3530d1b1a39 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -91,7 +91,8 @@ module TestEnv
'utf-16' => 'f05a987',
'gitaly-rename-test' => '94bb47c',
'smime-signed-commits' => 'ed775cc',
- 'Ääh-test-utf-8' => '7975be0'
+ 'Ääh-test-utf-8' => '7975be0',
+ 'ssh-signed-commit' => '7b5160f'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -312,12 +313,6 @@ module TestEnv
end
end
- def storage_dir_exists?(storage, dir)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- File.exist?(File.join(GitalySetup.repos_path(storage), dir))
- end
- end
-
def repos_path
@repos_path ||= GitalySetup.repos_path
end
@@ -375,6 +370,7 @@ module TestEnv
def seed_db
Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types
+ Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions
end
private
@@ -427,6 +423,8 @@ module TestEnv
return if File.exist?(install_dir) && ci?
if component_needs_update?(install_dir, version)
+ puts "==> Starting #{component} set up...\n"
+
# Cleanup the component entirely to ensure we start fresh
FileUtils.rm_rf(install_dir) if fresh_install
@@ -486,12 +484,14 @@ module TestEnv
# The HEAD of the component_folder will be used as heuristic for the version
# of the binaries, allowing to use Git to determine if HEAD is later than
# the expected version. Note: Git considers HEAD to be an anchestor of HEAD.
- _out, exit_status = Gitlab::Popen.popen(%W[
- #{Gitlab.config.git.bin_path}
- -C #{component_folder}
- merge-base --is-ancestor
- #{expected_version} HEAD
-])
+ _out, exit_status = Gitlab::Popen.popen(
+ %W[
+ #{Gitlab.config.git.bin_path}
+ -C #{component_folder}
+ merge-base --is-ancestor
+ #{expected_version} HEAD
+ ]
+ )
exit_status == 0
end
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 92a946db337..78ceaf297a8 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -2,124 +2,118 @@
module UsageDataHelpers
COUNTS_KEYS = %i(
- assignee_lists
- ci_builds
- ci_internal_pipelines
- ci_external_pipelines
- ci_pipeline_config_auto_devops
- ci_pipeline_config_repository
- ci_runners
- ci_triggers
- ci_pipeline_schedules
- auto_devops_enabled
- auto_devops_disabled
- deploy_keys
- deployments
- successful_deployments
- failed_deployments
- environments
- clusters
- clusters_enabled
- project_clusters_enabled
- group_clusters_enabled
- instance_clusters_enabled
- clusters_disabled
- project_clusters_disabled
- group_clusters_disabled
- instance_clusters_disabled
- clusters_platforms_eks
- clusters_platforms_gke
- clusters_platforms_user
- clusters_integrations_prometheus
- clusters_management_project
- in_review_folder
- grafana_integrated_projects
- groups
- issues
- issues_created_from_gitlab_error_tracking_ui
- issues_with_associated_zoom_link
- issues_using_zoom_quick_actions
- issues_with_embedded_grafana_charts_approx
- incident_issues
- keys
- label_lists
- labels
- lfs_objects
- merge_requests
- milestone_lists
- milestones
- notes
- pool_repositories
- projects
- projects_imported_from_github
- projects_asana_active
- projects_jenkins_active
- projects_jira_active
- projects_jira_server_active
- projects_jira_cloud_active
- projects_jira_dvcs_cloud_active
- projects_jira_dvcs_server_active
- projects_slack_active
- projects_slack_slash_commands_active
- projects_custom_issue_tracker_active
- projects_mattermost_active
- projects_prometheus_active
- projects_with_repositories_enabled
- projects_with_error_tracking_enabled
- projects_with_enabled_alert_integrations
- projects_with_expiration_policy_enabled_with_older_than_unset
- projects_with_expiration_policy_enabled_with_older_than_set_to_7d
- projects_with_expiration_policy_enabled_with_older_than_set_to_14d
- projects_with_expiration_policy_enabled_with_older_than_set_to_30d
- projects_with_expiration_policy_enabled_with_older_than_set_to_60d
- projects_with_expiration_policy_enabled_with_older_than_set_to_90d
- projects_with_terraform_reports
- projects_with_terraform_states
- pages_domains
- protected_branches
- protected_branches_except_default
- releases
- remote_mirrors
- snippets
- personal_snippets
- project_snippets
- suggestions
- terraform_reports
- terraform_states
- todos
- uploads
- web_hooks
- user_preferences_user_gitpod_enabled
- ).freeze
+ assignee_lists
+ ci_builds
+ ci_internal_pipelines
+ ci_external_pipelines
+ ci_pipeline_config_auto_devops
+ ci_pipeline_config_repository
+ ci_runners
+ ci_triggers
+ ci_pipeline_schedules
+ auto_devops_enabled
+ auto_devops_disabled
+ deploy_keys
+ deployments
+ successful_deployments
+ failed_deployments
+ environments
+ clusters
+ clusters_enabled
+ project_clusters_enabled
+ group_clusters_enabled
+ instance_clusters_enabled
+ clusters_disabled
+ project_clusters_disabled
+ group_clusters_disabled
+ instance_clusters_disabled
+ clusters_platforms_eks
+ clusters_platforms_gke
+ clusters_platforms_user
+ clusters_integrations_prometheus
+ clusters_management_project
+ in_review_folder
+ grafana_integrated_projects
+ groups
+ issues
+ issues_created_from_gitlab_error_tracking_ui
+ issues_with_associated_zoom_link
+ issues_using_zoom_quick_actions
+ issues_with_embedded_grafana_charts_approx
+ incident_issues
+ keys
+ label_lists
+ labels
+ lfs_objects
+ merge_requests
+ milestone_lists
+ milestones
+ notes
+ pool_repositories
+ projects
+ projects_imported_from_github
+ projects_asana_active
+ projects_jenkins_active
+ projects_jira_active
+ projects_jira_server_active
+ projects_jira_cloud_active
+ projects_jira_dvcs_cloud_active
+ projects_jira_dvcs_server_active
+ projects_slack_active
+ projects_slack_slash_commands_active
+ projects_custom_issue_tracker_active
+ projects_mattermost_active
+ projects_prometheus_active
+ projects_with_repositories_enabled
+ projects_with_error_tracking_enabled
+ projects_with_enabled_alert_integrations
+ projects_with_terraform_reports
+ projects_with_terraform_states
+ pages_domains
+ protected_branches
+ protected_branches_except_default
+ releases
+ remote_mirrors
+ snippets
+ personal_snippets
+ project_snippets
+ suggestions
+ terraform_reports
+ terraform_states
+ todos
+ uploads
+ web_hooks
+ user_preferences_user_gitpod_enabled
+ ).freeze
USAGE_DATA_KEYS = %i(
- active_user_count
- counts
- counts_monthly
- recorded_at
- edition
- version
- installation_type
- uuid
- hostname
- mattermost_enabled
- signup_enabled
- ldap_enabled
- gravatar_enabled
- omniauth_enabled
- reply_by_email_enabled
- container_registry_enabled
- dependency_proxy_enabled
- gitlab_shared_runners_enabled
- gitlab_pages
- git
- gitaly
- database
- prometheus_metrics_enabled
- web_ide_clientside_preview_enabled
- object_store
- topology
- ).freeze
+ active_user_count
+ counts
+ counts_monthly
+ recorded_at
+ edition
+ version
+ installation_type
+ uuid
+ hostname
+ mattermost_enabled
+ signup_enabled
+ ldap_enabled
+ gravatar_enabled
+ omniauth_enabled
+ reply_by_email_enabled
+ container_registry_enabled
+ dependency_proxy_enabled
+ gitlab_shared_runners_enabled
+ gitlab_pages
+ git
+ gitaly
+ database
+ prometheus_metrics_enabled
+ web_ide_clientside_preview_enabled
+ object_store
+ topology
+ ).freeze
def stub_usage_data_connections
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
@@ -162,7 +156,6 @@ module UsageDataHelpers
'direct_upload' => true,
'connection' =>
{ 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
- 'background_upload' => false,
'proxy_download' => false } }
)
@@ -177,7 +170,6 @@ module UsageDataHelpers
'direct_upload' => true,
'connection' =>
{ 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
- 'background_upload' => false,
'proxy_download' => false } }
)
allow(Settings).to receive(:[]).with('uploads')
@@ -188,7 +180,6 @@ module UsageDataHelpers
'direct_upload' => true,
'connection' =>
{ 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
- 'background_upload' => false,
'proxy_download' => false } }
)
allow(Settings).to receive(:[]).with('packages')
@@ -200,7 +191,6 @@ module UsageDataHelpers
'direct_upload' => false,
'connection' =>
{ 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
- 'background_upload' => true,
'proxy_download' => false } }
)
end
diff --git a/spec/support/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index 6f22df9ae0f..f894aff373c 100644
--- a/spec/support/helpers/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
@@ -39,11 +39,11 @@ module WorkhorseHelpers
# workhorse_finalize will transform file_key inside params as if it was the finalize call of an inline object storage upload.
# note that based on the content of the params it can simulate a disc acceleration or an object storage upload
- def workhorse_finalize(url, method: :post, file_key:, params:, headers: {}, send_rewritten_field: false)
+ def workhorse_finalize(url, file_key:, params:, method: :post, headers: {}, send_rewritten_field: false)
workhorse_finalize_with_multiple_files(url, method: method, file_keys: file_key, params: params, headers: headers, send_rewritten_field: send_rewritten_field)
end
- def workhorse_finalize_with_multiple_files(url, method: :post, file_keys:, params:, headers: {}, send_rewritten_field: false)
+ def workhorse_finalize_with_multiple_files(url, file_keys:, params:, method: :post, headers: {}, send_rewritten_field: false)
workhorse_request_with_multiple_files(method, url,
file_keys: file_keys,
params: params,
@@ -52,11 +52,11 @@ module WorkhorseHelpers
)
end
- def workhorse_request_with_file(method, url, file_key:, params:, env: {}, extra_headers: {}, send_rewritten_field:)
+ def workhorse_request_with_file(method, url, file_key:, params:, send_rewritten_field:, env: {}, extra_headers: {})
workhorse_request_with_multiple_files(method, url, file_keys: file_key, params: params, env: env, extra_headers: extra_headers, send_rewritten_field: send_rewritten_field)
end
- def workhorse_request_with_multiple_files(method, url, file_keys:, params:, env: {}, extra_headers: {}, send_rewritten_field:)
+ def workhorse_request_with_multiple_files(method, url, file_keys:, params:, send_rewritten_field:, env: {}, extra_headers: {})
workhorse_params = params.dup
file_keys = Array(file_keys)
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
index 9da151895a7..3d7a0d29e71 100644
--- a/spec/support/import_export/common_util.rb
+++ b/spec/support/import_export/common_util.rb
@@ -83,7 +83,7 @@ module ImportExport
path = File.join(dir_path, "#{exportable_path}.json")
return unless File.exist?(path)
- Gitlab::Json.parse(IO.read(path))
+ Gitlab::Json.parse(File.read(path))
end
def consume_relations(dir_path, exportable_path, key)
@@ -101,7 +101,7 @@ module ImportExport
end
def project_json(filename)
- Gitlab::Json.parse(IO.read(filename))
+ Gitlab::Json.parse(File.read(filename))
end
end
end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index 3134e5c32a3..9a26f50903f 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -117,8 +117,8 @@ module ExportFileHelper
# Check whether this is a hash attribute inside a model
if model.is_a?(Symbol)
return true if (safe_hashes[model] - parent.keys).empty?
- else
- return true if safe_model?(model, excluded_attributes, parent)
+ elsif safe_model?(model, excluded_attributes, parent)
+ return true
end
end
diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb
index 6d7658b7c33..a5a017828b3 100644
--- a/spec/support/matchers/exceed_query_limit.rb
+++ b/spec/support/matchers/exceed_query_limit.rb
@@ -63,14 +63,16 @@ module ExceedQueryLimitHelpers
end
end
- MARGINALIA_ANNOTATION_REGEX = %r{\s*\/\*.*\*\/}.freeze
-
- DB_QUERY_RE = Regexp.union([
- /^(?<prefix>SELECT .* FROM "?[a-z_]+"?) (?<suffix>.*)$/m,
- /^(?<prefix>UPDATE "?[a-z_]+"?) (?<suffix>.*)$/m,
- /^(?<prefix>INSERT INTO "[a-z_]+" \((?:"[a-z_]+",?\s?)+\)) (?<suffix>.*)$/m,
- /^(?<prefix>DELETE FROM "[a-z_]+") (?<suffix>.*)$/m
- ]).freeze
+ MARGINALIA_ANNOTATION_REGEX = %r{\s*/\*.*\*/}.freeze
+
+ DB_QUERY_RE = Regexp.union(
+ [
+ /^(?<prefix>SELECT .* FROM "?[a-z_]+"?) (?<suffix>.*)$/m,
+ /^(?<prefix>UPDATE "?[a-z_]+"?) (?<suffix>.*)$/m,
+ /^(?<prefix>INSERT INTO "[a-z_]+" \((?:"[a-z_]+",?\s?)+\)) (?<suffix>.*)$/m,
+ /^(?<prefix>DELETE FROM "[a-z_]+") (?<suffix>.*)$/m
+ ]
+ ).freeze
def with_threshold(threshold)
@threshold = threshold
diff --git a/spec/support/memory_instrumentation_helper.rb b/spec/support/memory_instrumentation_helper.rb
index 84ec02fa5aa..51506376a75 100644
--- a/spec/support/memory_instrumentation_helper.rb
+++ b/spec/support/memory_instrumentation_helper.rb
@@ -5,13 +5,10 @@
# This concept is currently tried to be upstreamed here:
# - https://github.com/ruby/ruby/pull/3978
module MemoryInstrumentationHelper
- def skip_memory_instrumentation!
+ def verify_memory_instrumentation_available!
return if ::Gitlab::Memory::Instrumentation.available?
- # if we are running in CI, a test cannot be skipped
- return if ENV['CI']
-
- skip 'Missing a memory instrumentation patch. ' \
+ raise 'Ruby is missing a required patch that enables memory instrumentation. ' \
'More information can be found here: https://gitlab.com/gitlab-org/gitlab/-/issues/296530.'
end
end
diff --git a/spec/support/migration.rb b/spec/support/migration.rb
index 4d4a293e9ff..b1e75d9c9e2 100644
--- a/spec/support/migration.rb
+++ b/spec/support/migration.rb
@@ -20,6 +20,14 @@ RSpec.configure do |config|
Gitlab::CurrentSettings.clear_in_memory_application_settings!
end
+ config.prepend_before(:all, :migration) do
+ TestProf::BeforeAll.adapter = ::TestProfBeforeAllAdapter.no_transaction_adapter
+ end
+
+ config.append_after(:all, :migration) do
+ TestProf::BeforeAll.adapter = ::TestProfBeforeAllAdapter.default_adapter
+ end
+
config.append_after(:context, :migration) do
recreate_databases_and_seed_if_needed || ensure_schema_and_empty_tables
end
diff --git a/spec/support/migrations_helpers/vulnerabilities_findings_helper.rb b/spec/support/migrations_helpers/vulnerabilities_findings_helper.rb
index a3cccc3a75d..9a5313c3fa4 100644
--- a/spec/support/migrations_helpers/vulnerabilities_findings_helper.rb
+++ b/spec/support/migrations_helpers/vulnerabilities_findings_helper.rb
@@ -92,10 +92,10 @@ module MigrationHelpers
"url" => "http://goat:8080/WebGoat/logout",
"body" => "",
"headers" => [
- {
- "name" => "Accept",
- "value" => "*/*"
- }
+ {
+ "name" => "Accept",
+ "value" => "*/*"
+ }
]
},
"response" => {
diff --git a/spec/support/models/ci/partitioning_testing/cascade_check.rb b/spec/support/models/ci/partitioning_testing/cascade_check.rb
index f553a47ef4f..bcfc9675476 100644
--- a/spec/support/models/ci/partitioning_testing/cascade_check.rb
+++ b/spec/support/models/ci/partitioning_testing/cascade_check.rb
@@ -15,6 +15,13 @@ module PartitioningTesting
raise "partition_id was expected to equal #{partition_scope_value} but it was #{partition_id}."
end
+
+ class_methods do
+ # Allowing partition callback to be used with BulkInsertSafe
+ def _bulk_insert_callback_allowed?(name, args)
+ super || args.first == :after && args.second == :check_partition_cascade_value
+ end
+ end
end
end
diff --git a/spec/support/models/ci/partitioning_testing/schema_helpers.rb b/spec/support/models/ci/partitioning_testing/schema_helpers.rb
index 712178710da..3a79ed1b5a9 100644
--- a/spec/support/models/ci/partitioning_testing/schema_helpers.rb
+++ b/spec/support/models/ci/partitioning_testing/schema_helpers.rb
@@ -8,10 +8,10 @@ module Ci
module_function
def with_routing_tables
- Ci::BuildMetadata.table_name = :p_ci_builds_metadata
+ # model.table_name = :routing_table
yield
- ensure
- Ci::BuildMetadata.table_name = :ci_builds_metadata
+ # ensure
+ # model.table_name = :regular_table
end
# We're dropping the default values here to ensure that the application code
diff --git a/spec/support/patches/rspec_mocks_prepended_methods.rb b/spec/support/patches/rspec_mocks_prepended_methods.rb
index fa3a74c670c..d51fb37a499 100644
--- a/spec/support/patches/rspec_mocks_prepended_methods.rb
+++ b/spec/support/patches/rspec_mocks_prepended_methods.rb
@@ -45,7 +45,7 @@ module RSpec
private
def method_owner
- @method_owner ||= Object.instance_method(:method).bind(object).call(@method_name).owner
+ @method_owner ||= Object.instance_method(:method).bind_call(object, @method_name).owner
end
end
end
diff --git a/spec/support/prometheus/additional_metrics_shared_examples.rb b/spec/support/prometheus/additional_metrics_shared_examples.rb
index 3a5909cd908..e589baf0909 100644
--- a/spec/support/prometheus/additional_metrics_shared_examples.rb
+++ b/spec/support/prometheus/additional_metrics_shared_examples.rb
@@ -92,15 +92,15 @@ RSpec.shared_examples 'additional metrics query' do
metrics: [
{
title: 'title', weight: 1, y_label: 'Values', queries: [
- { query_range: 'query_range_a', result: query_range_result },
- { query_range: 'query_range_b', label: 'label', unit: 'unit', result: query_range_result }
- ]
+ { query_range: 'query_range_a', result: query_range_result },
+ { query_range: 'query_range_b', label: 'label', unit: 'unit', result: query_range_result }
+ ]
}
]
}
]
- expect(query_result).to match_schema('prometheus/additional_metrics_query_result')
+ expect(query_result.to_json).to match_schema('prometheus/additional_metrics_query_result')
expect(query_result).to eq(expected)
end
end
@@ -128,7 +128,7 @@ RSpec.shared_examples 'additional metrics query' do
queries_with_result_a = { queries: [{ query_range: 'query_range_a', result: query_range_result }] }
queries_with_result_b = { queries: [{ query_range: 'query_range_b', result: query_range_result }] }
- expect(query_result).to match_schema('prometheus/additional_metrics_query_result')
+ expect(query_result.to_json).to match_schema('prometheus/additional_metrics_query_result')
expect(query_result.count).to eq(2)
expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 })
@@ -147,7 +147,7 @@ RSpec.shared_examples 'additional metrics query' do
it 'return group data only for query with results' do
queries_with_result = { queries: [{ query_range: 'query_range_a', result: query_range_result }] }
- expect(query_result).to match_schema('prometheus/additional_metrics_query_result')
+ expect(query_result.to_json).to match_schema('prometheus/additional_metrics_query_result')
expect(query_result.count).to eq(1)
expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 })
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index 33945509675..0368fd63357 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -4,6 +4,7 @@ RSpec.shared_examples "redis_shared_examples" do
include StubENV
let(:test_redis_url) { "redis://redishost:#{redis_port}" }
+ let(:test_cluster_config) { { cluster: [{ host: "redis://redishost", port: redis_port }] } }
let(:config_file_name) { instance_specific_config_file }
let(:config_old_format_socket) { "spec/fixtures/config/redis_old_format_socket.yml" }
let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
@@ -11,6 +12,7 @@ RSpec.shared_examples "redis_shared_examples" do
let(:new_socket_path) { "/path/to/redis.sock" }
let(:config_old_format_host) { "spec/fixtures/config/redis_old_format_host.yml" }
let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
+ let(:config_cluster_format_host) { "spec/fixtures/config/redis_cluster_format_host.yml" }
let(:redis_port) { 6379 }
let(:redis_database) { 99 }
let(:sentinel_port) { 26379 }
@@ -191,6 +193,30 @@ RSpec.shared_examples "redis_shared_examples" do
end
end
end
+
+ context 'with redis cluster format' do
+ let(:config_file_name) { config_cluster_format_host }
+
+ where(:rails_env, :host) do
+ [
+ %w[development development-master],
+ %w[test test-master],
+ %w[production production-master]
+ ]
+ end
+
+ with_them do
+ it 'returns hash with cluster and password' do
+ is_expected.to include(password: 'myclusterpassword',
+ cluster: [
+ { host: "#{host}1", port: redis_port },
+ { host: "#{host}2", port: redis_port }
+ ]
+ )
+ is_expected.not_to have_key(:url)
+ end
+ end
+ end
end
end
@@ -317,6 +343,14 @@ RSpec.shared_examples "redis_shared_examples" do
expect(subject).to eq(redis_database)
end
end
+
+ context 'with cluster-mode' do
+ let(:config_file_name) { config_cluster_format_host }
+
+ it 'returns the correct db' do
+ expect(subject).to eq(0)
+ end
+ end
end
describe '#sentinels' do
@@ -350,6 +384,14 @@ RSpec.shared_examples "redis_shared_examples" do
is_expected.to be_nil
end
end
+
+ context 'when cluster is defined' do
+ let(:config_file_name) { config_cluster_format_host }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
end
describe '#sentinels?' do
@@ -370,6 +412,14 @@ RSpec.shared_examples "redis_shared_examples" do
is_expected.to be_falsey
end
end
+
+ context 'when cluster is defined' do
+ let(:config_file_name) { config_cluster_format_host }
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+ end
end
describe '#raw_config_hash' do
@@ -377,6 +427,11 @@ RSpec.shared_examples "redis_shared_examples" do
expect(subject).to receive(:fetch_config) { test_redis_url }
expect(subject.send(:raw_config_hash)).to eq(url: test_redis_url)
end
+
+ it 'returns cluster config without url key in a hash' do
+ expect(subject).to receive(:fetch_config) { test_cluster_config }
+ expect(subject.send(:raw_config_hash)).to eq(test_cluster_config)
+ end
end
describe '#fetch_config' do
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index 71dfc3fd5a3..ff0b5bebe33 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -8,6 +8,8 @@ require_relative "helpers/stub_object_storage"
require_relative "helpers/stub_env"
require_relative "helpers/fast_rails_root"
+require_relative "../../lib/gitlab/utils"
+
RSpec::Expectations.configuration.on_potential_false_positives = :raise
RSpec.configure do |config|
@@ -35,4 +37,13 @@ RSpec.configure do |config|
config.include StubObjectStorage
config.include StubENV
config.include FastRailsRoot
+
+ warn_missing_feature_category = Gitlab::Utils.to_boolean(ENV['RSPEC_WARN_MISSING_FEATURE_CATEGORY'], default: true)
+
+ # Add warning for example missing feature_category
+ config.before do |example|
+ if warn_missing_feature_category && example.metadata[:feature_category].blank? && !ENV['CI']
+ warn "Missing metadata feature_category: #{example.location} See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#feature-category-metadata"
+ end
+ end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 67b7023f1ff..489ed89c048 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -5,9 +5,6 @@
#
---
- './ee/spec/components/billing/plan_component_spec.rb'
-- './ee/spec/components/namespaces/free_user_cap/alert_component_spec.rb'
-- './ee/spec/components/namespaces/free_user_cap/preview_alert_component_spec.rb'
-- './ee/spec/components/namespaces/free_user_cap/preview_usage_quota_alert_component_spec.rb'
- './ee/spec/components/namespaces/free_user_cap/usage_quota_alert_component_spec.rb'
- './ee/spec/components/namespaces/free_user_cap/usage_quota_trial_alert_component_spec.rb'
- './ee/spec/components/namespaces/storage/limit_alert_component_spec.rb'
@@ -137,8 +134,6 @@
- './ee/spec/controllers/projects/environments_controller_spec.rb'
- './ee/spec/controllers/projects/feature_flag_issues_controller_spec.rb'
- './ee/spec/controllers/projects/imports_controller_spec.rb'
-- './ee/spec/controllers/projects/incident_management/escalation_policies_controller_spec.rb'
-- './ee/spec/controllers/projects/incident_management/oncall_schedules_controller_spec.rb'
- './ee/spec/controllers/projects/insights_controller_spec.rb'
- './ee/spec/controllers/projects/integrations/jira/issues_controller_spec.rb'
- './ee/spec/controllers/projects/integrations/zentao/issues_controller_spec.rb'
@@ -262,7 +257,6 @@
- './ee/spec/features/boards/user_visits_board_spec.rb'
- './ee/spec/features/burndown_charts_spec.rb'
- './ee/spec/features/burnup_charts_spec.rb'
-- './ee/spec/features/ci/ci_minutes_spec.rb'
- './ee/spec/features/ci_shared_runner_settings_spec.rb'
- './ee/spec/features/ci_shared_runner_warnings_spec.rb'
- './ee/spec/features/clusters/cluster_detail_page_spec.rb'
@@ -353,8 +347,6 @@
- './ee/spec/features/groups/wiki/user_views_wiki_empty_spec.rb'
- './ee/spec/features/ide/user_commits_changes_spec.rb'
- './ee/spec/features/ide/user_opens_ide_spec.rb'
-- './ee/spec/features/incidents/incident_details_spec.rb'
-- './ee/spec/features/incidents/incidents_list_spec.rb'
- './ee/spec/features/integrations/jira/jira_issues_list_spec.rb'
- './ee/spec/features/invites_spec.rb'
- './ee/spec/features/issues/blocking_issues_spec.rb'
@@ -421,7 +413,6 @@
- './ee/spec/features/profiles/account_spec.rb'
- './ee/spec/features/profiles/billing_spec.rb'
- './ee/spec/features/profiles/password_spec.rb'
-- './ee/spec/features/profiles/usage_quotas_spec.rb'
- './ee/spec/features/profiles/user_visits_public_profile_spec.rb'
- './ee/spec/features/projects/active_tabs_spec.rb'
- './ee/spec/features/projects/audit_events_spec.rb'
@@ -592,13 +583,6 @@
- './ee/spec/finders/group_projects_finder_spec.rb'
- './ee/spec/finders/group_saml_identity_finder_spec.rb'
- './ee/spec/finders/groups_with_templates_finder_spec.rb'
-- './ee/spec/finders/incident_management/escalation_policies_finder_spec.rb'
-- './ee/spec/finders/incident_management/escalation_rules_finder_spec.rb'
-- './ee/spec/finders/incident_management/issuable_resource_links_finder_spec.rb'
-- './ee/spec/finders/incident_management/member_oncall_rotations_finder_spec.rb'
-- './ee/spec/finders/incident_management/oncall_rotations_finder_spec.rb'
-- './ee/spec/finders/incident_management/oncall_schedules_finder_spec.rb'
-- './ee/spec/finders/incident_management/oncall_users_finder_spec.rb'
- './ee/spec/finders/issues_finder_spec.rb'
- './ee/spec/finders/iterations/cadences_finder_spec.rb'
- './ee/spec/finders/iterations_finder_spec.rb'
@@ -625,8 +609,6 @@
- './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/status_page/incident_comments_finder_spec.rb'
-- './ee/spec/finders/status_page/incidents_finder_spec.rb'
- './ee/spec/finders/template_finder_spec.rb'
- './ee/spec/finders/users_finder_spec.rb'
- './ee/spec/frontend/fixtures/analytics/charts.rb'
@@ -714,17 +696,6 @@
- './ee/spec/graphql/mutations/epics/create_spec.rb'
- './ee/spec/graphql/mutations/epics/update_spec.rb'
- './ee/spec/graphql/mutations/gitlab_subscriptions/activate_spec.rb'
-- './ee/spec/graphql/mutations/incident_management/escalation_policy/create_spec.rb'
-- './ee/spec/graphql/mutations/incident_management/escalation_policy/destroy_spec.rb'
-- './ee/spec/graphql/mutations/incident_management/escalation_policy/update_spec.rb'
-- './ee/spec/graphql/mutations/incident_management/issuable_resource_link/create_spec.rb'
-- './ee/spec/graphql/mutations/incident_management/issuable_resource_link/destroy_spec.rb'
-- './ee/spec/graphql/mutations/incident_management/oncall_rotation/create_spec.rb'
-- './ee/spec/graphql/mutations/incident_management/oncall_rotation/destroy_spec.rb'
-- './ee/spec/graphql/mutations/incident_management/oncall_rotation/update_spec.rb'
-- './ee/spec/graphql/mutations/incident_management/oncall_schedule/create_spec.rb'
-- './ee/spec/graphql/mutations/incident_management/oncall_schedule/destroy_spec.rb'
-- './ee/spec/graphql/mutations/incident_management/oncall_schedule/update_spec.rb'
- './ee/spec/graphql/mutations/instance_security_dashboard/add_project_spec.rb'
- './ee/spec/graphql/mutations/instance_security_dashboard/remove_project_spec.rb'
- './ee/spec/graphql/mutations/issues/create_spec.rb'
@@ -796,12 +767,6 @@
- './ee/spec/graphql/resolvers/geo/snippet_repository_registries_resolver_spec.rb'
- './ee/spec/graphql/resolvers/geo/terraform_state_version_registries_resolver_spec.rb'
- './ee/spec/graphql/resolvers/geo/upload_registries_resolver_spec.rb'
-- './ee/spec/graphql/resolvers/incident_management/escalation_policies_resolver_spec.rb'
-- './ee/spec/graphql/resolvers/incident_management/issuable_resource_links_resolver_spec.rb'
-- './ee/spec/graphql/resolvers/incident_management/oncall_rotations_resolver_spec.rb'
-- './ee/spec/graphql/resolvers/incident_management/oncall_schedule_resolver_spec.rb'
-- './ee/spec/graphql/resolvers/incident_management/oncall_shifts_resolver_spec.rb'
-- './ee/spec/graphql/resolvers/incident_management/oncall_users_resolver_spec.rb'
- './ee/spec/graphql/resolvers/instance_security_dashboard/projects_resolver_spec.rb'
- './ee/spec/graphql/resolvers/instance_security_dashboard_resolver_spec.rb'
- './ee/spec/graphql/resolvers/iterations/cadences_resolver_spec.rb'
@@ -899,16 +864,6 @@
- './ee/spec/graphql/types/group_release_stats_type_spec.rb'
- './ee/spec/graphql/types/group_stats_type_spec.rb'
- './ee/spec/graphql/types/health_status_enum_spec.rb'
-- './ee/spec/graphql/types/incident_management/escalation_policy_type_spec.rb'
-- './ee/spec/graphql/types/incident_management/escalation_rule_input_type_spec.rb'
-- './ee/spec/graphql/types/incident_management/escalation_rule_type_spec.rb'
-- './ee/spec/graphql/types/incident_management/issuable_resource_link_type_enum_spec.rb'
-- './ee/spec/graphql/types/incident_management/issuable_resource_link_type_spec.rb'
-- './ee/spec/graphql/types/incident_management/oncall_participant_type_spec.rb'
-- './ee/spec/graphql/types/incident_management/oncall_rotation_date_input_type_spec.rb'
-- './ee/spec/graphql/types/incident_management/oncall_rotation_type_spec.rb'
-- './ee/spec/graphql/types/incident_management/oncall_schedule_type_spec.rb'
-- './ee/spec/graphql/types/incident_management/oncall_shift_type_spec.rb'
- './ee/spec/graphql/types/instance_security_dashboard_type_spec.rb'
- './ee/spec/graphql/types/issue_connection_type_spec.rb'
- './ee/spec/graphql/types/issue_type_spec.rb'
@@ -1049,7 +1004,6 @@
- './ee/spec/helpers/ee/operations_helper_spec.rb'
- './ee/spec/helpers/ee/personal_access_tokens_helper_spec.rb'
- './ee/spec/helpers/ee/profiles_helper_spec.rb'
-- './ee/spec/helpers/ee/projects/incidents_helper_spec.rb'
- './ee/spec/helpers/ee/projects/pipeline_helper_spec.rb'
- './ee/spec/helpers/ee/projects/security/api_fuzzing_configuration_helper_spec.rb'
- './ee/spec/helpers/ee/projects/security/configuration_helper_spec.rb'
@@ -1074,8 +1028,6 @@
- './ee/spec/helpers/groups/ldap_sync_helper_spec.rb'
- './ee/spec/helpers/groups/security_features_helper_spec.rb'
- './ee/spec/helpers/groups/sso_helper_spec.rb'
-- './ee/spec/helpers/incident_management/escalation_policy_helper_spec.rb'
-- './ee/spec/helpers/incident_management/oncall_schedule_helper_spec.rb'
- './ee/spec/helpers/kerberos_helper_spec.rb'
- './ee/spec/helpers/license_helper_spec.rb'
- './ee/spec/helpers/license_monitoring_helper_spec.rb'
@@ -1525,7 +1477,6 @@
- './ee/spec/lib/gitlab/geo/log_cursor/daemon_spec.rb'
- './ee/spec/lib/gitlab/geo/log_cursor/event_logs_spec.rb'
- './ee/spec/lib/gitlab/geo/log_cursor/events/cache_invalidation_event_spec.rb'
-- './ee/spec/lib/gitlab/geo/log_cursor/events/container_repository_updated_event_spec.rb'
- './ee/spec/lib/gitlab/geo/log_cursor/events/design_repository_updated_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'
@@ -1572,7 +1523,6 @@
- './ee/spec/lib/gitlab/import_export/group/relation_factory_spec.rb'
- './ee/spec/lib/gitlab/import_export/project/object_builder_spec.rb'
- './ee/spec/lib/gitlab/import_sources_spec.rb'
-- './ee/spec/lib/gitlab/incident_management_spec.rb'
- './ee/spec/lib/gitlab/ingestion/bulk_insertable_task_spec.rb'
- './ee/spec/lib/gitlab/insights/executors/dora_executor_spec.rb'
- './ee/spec/lib/gitlab/insights/executors/issuable_executor_spec.rb'
@@ -1629,14 +1579,6 @@
- './ee/spec/lib/gitlab/slash_commands/presenters/issue_show_spec.rb'
- './ee/spec/lib/gitlab/spdx/catalogue_gateway_spec.rb'
- './ee/spec/lib/gitlab/spdx/catalogue_spec.rb'
-- './ee/spec/lib/gitlab/status_page/filter/image_filter_spec.rb'
-- './ee/spec/lib/gitlab/status_page/filter/mention_anonymization_filter_spec.rb'
-- './ee/spec/lib/gitlab/status_page/pipeline/post_process_pipeline_spec.rb'
-- './ee/spec/lib/gitlab/status_page_spec.rb'
-- './ee/spec/lib/gitlab/status_page/storage/s3_client_spec.rb'
-- './ee/spec/lib/gitlab/status_page/storage/s3_multipart_upload_spec.rb'
-- './ee/spec/lib/gitlab/status_page/storage_spec.rb'
-- './ee/spec/lib/gitlab/status_page/usage_data_counters/incident_counter_spec.rb'
- './ee/spec/lib/gitlab/subscription_portal/clients/graphql_spec.rb'
- './ee/spec/lib/gitlab/subscription_portal/client_spec.rb'
- './ee/spec/lib/gitlab/subscription_portal/clients/rest_spec.rb'
@@ -1686,7 +1628,6 @@
- './ee/spec/lib/gitlab/web_ide/config/entry/schema_spec.rb'
- './ee/spec/lib/gitlab/web_ide/config/entry/schemas_spec.rb'
- './ee/spec/lib/gitlab/web_ide/config/entry/schema/uri_spec.rb'
-- './ee/spec/lib/incident_management/oncall_shift_generator_spec.rb'
- './ee/spec/lib/omni_auth/strategies/group_saml_spec.rb'
- './ee/spec/lib/omni_auth/strategies/kerberos_spec.rb'
- './ee/spec/lib/peek/views/elasticsearch_spec.rb'
@@ -1830,7 +1771,6 @@
- './ee/spec/models/concerns/geo/verifiable_model_spec.rb'
- './ee/spec/models/concerns/geo/verification_state_spec.rb'
- './ee/spec/models/concerns/health_status_spec.rb'
-- './ee/spec/models/concerns/incident_management/base_pending_escalation_spec.rb'
- './ee/spec/models/concerns/password_complexity_spec.rb'
- './ee/spec/models/concerns/scim_paginatable_spec.rb'
- './ee/spec/models/container_registry/event_spec.rb'
@@ -1874,7 +1814,6 @@
- './ee/spec/models/ee/group_group_link_spec.rb'
- './ee/spec/models/ee/groups/feature_setting_spec.rb'
- './ee/spec/models/ee/group_spec.rb'
-- './ee/spec/models/ee/incident_management/project_incident_management_setting_spec.rb'
- './ee/spec/models/ee/integrations/jira_spec.rb'
- './ee/spec/models/ee/integration_spec.rb'
- './ee/spec/models/ee/iterations/cadence_spec.rb'
@@ -1930,7 +1869,6 @@
- './ee/spec/models/geo/cache_invalidation_event_spec.rb'
- './ee/spec/models/geo/ci_secure_file_registry_spec.rb'
- './ee/spec/models/geo/container_repository_registry_spec.rb'
-- './ee/spec/models/geo/container_repository_updated_event_spec.rb'
- './ee/spec/models/geo/deleted_project_spec.rb'
- './ee/spec/models/geo/design_registry_spec.rb'
- './ee/spec/models/geo/event_log_spec.rb'
@@ -1974,16 +1912,6 @@
- './ee/spec/models/historical_data_spec.rb'
- './ee/spec/models/hooks/group_hook_spec.rb'
- './ee/spec/models/identity_spec.rb'
-- './ee/spec/models/incident_management/escalation_policy_spec.rb'
-- './ee/spec/models/incident_management/escalation_rule_spec.rb'
-- './ee/spec/models/incident_management/issuable_escalation_status_spec.rb'
-- './ee/spec/models/incident_management/issuable_resource_link_spec.rb'
-- './ee/spec/models/incident_management/oncall_participant_spec.rb'
-- './ee/spec/models/incident_management/oncall_rotation_spec.rb'
-- './ee/spec/models/incident_management/oncall_schedule_spec.rb'
-- './ee/spec/models/incident_management/oncall_shift_spec.rb'
-- './ee/spec/models/incident_management/pending_escalations/alert_spec.rb'
-- './ee/spec/models/incident_management/pending_escalations/issue_spec.rb'
- './ee/spec/models/instance_security_dashboard_spec.rb'
- './ee/spec/models/integrations/chat_message/vulnerability_message_spec.rb'
- './ee/spec/models/integrations/github/remote_project_spec.rb'
@@ -2069,8 +1997,6 @@
- './ee/spec/models/snippet_spec.rb'
- './ee/spec/models/software_license_policy_spec.rb'
- './ee/spec/models/software_license_spec.rb'
-- './ee/spec/models/status_page/project_setting_spec.rb'
-- './ee/spec/models/status_page/published_incident_spec.rb'
- './ee/spec/models/storage_shard_spec.rb'
- './ee/spec/models/uploads/local_spec.rb'
- './ee/spec/models/upload_spec.rb'
@@ -2133,9 +2059,6 @@
- './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/incident_management/oncall_rotation_policy_spec.rb'
-- './ee/spec/policies/incident_management/oncall_schedule_policy_spec.rb'
-- './ee/spec/policies/incident_management/oncall_shift_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'
@@ -2267,7 +2190,6 @@
- './ee/spec/requests/api/graphql/group/epics_spec.rb'
- './ee/spec/requests/api/graphql/group/external_audit_event_destinations_spec.rb'
- './ee/spec/requests/api/graphql/group_query_spec.rb'
-- './ee/spec/requests/api/graphql/incident_management/issuable_resource_links_spec.rb'
- './ee/spec/requests/api/graphql/instance_security_dashboard_spec.rb'
- './ee/spec/requests/api/graphql/iterations/cadences_spec.rb'
- './ee/spec/requests/api/graphql/iterations/iterations_spec.rb'
@@ -2321,16 +2243,6 @@
- './ee/spec/requests/api/graphql/mutations/epics/update_spec.rb'
- './ee/spec/requests/api/graphql/mutations/epic_tree/reorder_spec.rb'
- './ee/spec/requests/api/graphql/mutations/gitlab_subscriptions/activate_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/incident_management/escalation_policy/create_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/incident_management/escalation_policy/destroy_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/incident_management/escalation_policy/update_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/incident_management/issuable_resource_link/create_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/incident_management/issuable_resource_link/destroy_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/incident_management/oncall_rotation/create_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/incident_management/oncall_rotation/update_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/incident_management/oncall_schedule/create_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/incident_management/oncall_schedule/destroy_spec.rb'
-- './ee/spec/requests/api/graphql/mutations/incident_management/oncall_schedule/update_spec.rb'
- './ee/spec/requests/api/graphql/mutations/issues/create_spec.rb'
- './ee/spec/requests/api/graphql/mutations/issues/promote_to_epic_spec.rb'
- './ee/spec/requests/api/graphql/mutations/issues/set_epic_spec.rb'
@@ -2378,11 +2290,6 @@
- './ee/spec/requests/api/graphql/project/dast_site_profile_spec.rb'
- './ee/spec/requests/api/graphql/project/dast_site_profiles_spec.rb'
- './ee/spec/requests/api/graphql/project/dast_site_validations_spec.rb'
-- './ee/spec/requests/api/graphql/project/incident_management/escalation_policies_spec.rb'
-- './ee/spec/requests/api/graphql/project/incident_management/escalation_policy/rules_spec.rb'
-- './ee/spec/requests/api/graphql/project/incident_management/oncall_participants_spec.rb'
-- './ee/spec/requests/api/graphql/project/incident_management/oncall_schedules_spec.rb'
-- './ee/spec/requests/api/graphql/project/incident_management/oncall_shifts_spec.rb'
- './ee/spec/requests/api/graphql/project/issues_spec.rb'
- './ee/spec/requests/api/graphql/project/merge_requests_spec.rb'
- './ee/spec/requests/api/graphql/project/path_locks_spec.rb'
@@ -2501,14 +2408,12 @@
- './ee/spec/requests/groups/roadmap_controller_spec.rb'
- './ee/spec/requests/groups/security/credentials_controller_spec.rb'
- './ee/spec/requests/groups/settings/reporting_controller_spec.rb'
-- './ee/spec/requests/groups/usage_quotas_spec.rb'
- './ee/spec/requests/jwt_controller_spec.rb'
- './ee/spec/requests/lfs_http_spec.rb'
- './ee/spec/requests/lfs_locks_api_spec.rb'
- './ee/spec/requests/omniauth_kerberos_spec.rb'
- './ee/spec/requests/projects/analytics/code_reviews_controller_spec.rb'
- './ee/spec/requests/projects/audit_events_spec.rb'
-- './ee/spec/requests/projects/incidents_controller_spec.rb'
- './ee/spec/requests/projects/issue_feature_flags_controller_spec.rb'
- './ee/spec/requests/projects/issues_controller_spec.rb'
- './ee/spec/requests/projects/merge_requests_controller_spec.rb'
@@ -2588,8 +2493,6 @@
- './ee/spec/serializers/fork_namespace_entity_spec.rb'
- './ee/spec/serializers/geo_project_registry_entity_spec.rb'
- './ee/spec/serializers/group_vulnerability_autocomplete_entity_spec.rb'
-- './ee/spec/serializers/incident_management/escalation_policy_entity_spec.rb'
-- './ee/spec/serializers/incident_management/oncall_schedule_entity_spec.rb'
- './ee/spec/serializers/integrations/field_entity_spec.rb'
- './ee/spec/serializers/integrations/jira_serializers/issue_detail_entity_spec.rb'
- './ee/spec/serializers/integrations/jira_serializers/issue_entity_spec.rb'
@@ -2620,10 +2523,6 @@
- './ee/spec/serializers/security/license_policy_entity_spec.rb'
- './ee/spec/serializers/security/vulnerability_report_data_entity_spec.rb'
- './ee/spec/serializers/security/vulnerability_report_data_serializer_spec.rb'
-- './ee/spec/serializers/status_page/incident_comment_entity_spec.rb'
-- './ee/spec/serializers/status_page/incident_entity_spec.rb'
-- './ee/spec/serializers/status_page/incident_serializer_spec.rb'
-- './ee/spec/serializers/status_page/renderer_spec.rb'
- './ee/spec/serializers/storage_shard_entity_spec.rb'
- './ee/spec/serializers/test_reports_comparer_entity_spec.rb'
- './ee/spec/serializers/test_reports_comparer_serializer_spec.rb'
@@ -2836,9 +2735,6 @@
- './ee/spec/services/ee/groups/deploy_tokens/revoke_service_spec.rb'
- './ee/spec/services/ee/groups/import_export/export_service_spec.rb'
- './ee/spec/services/ee/groups/import_export/import_service_spec.rb'
-- './ee/spec/services/ee/incident_management/issuable_escalation_statuses/after_update_service_spec.rb'
-- './ee/spec/services/ee/incident_management/issuable_escalation_statuses/create_service_spec.rb'
-- './ee/spec/services/ee/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb'
- './ee/spec/services/ee/integrations/test/project_service_spec.rb'
- './ee/spec/services/ee/ip_restrictions/update_service_spec.rb'
- './ee/spec/services/ee/issuable/bulk_update_service_spec.rb'
@@ -2956,7 +2852,6 @@
- './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/container_repository_updated_event_store_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'
@@ -3030,26 +2925,6 @@
- './ee/spec/services/groups/update_service_spec.rb'
- './ee/spec/services/historical_user_data/csv_service_spec.rb'
- './ee/spec/services/ide/schemas_config_service_spec.rb'
-- './ee/spec/services/incident_management/create_incident_sla_exceeded_label_service_spec.rb'
-- './ee/spec/services/incident_management/escalation_policies/create_service_spec.rb'
-- './ee/spec/services/incident_management/escalation_policies/destroy_service_spec.rb'
-- './ee/spec/services/incident_management/escalation_policies/update_service_spec.rb'
-- './ee/spec/services/incident_management/escalation_rules/destroy_service_spec.rb'
-- './ee/spec/services/incident_management/incidents/create_sla_service_spec.rb'
-- './ee/spec/services/incident_management/incidents/upload_metric_service_spec.rb'
-- './ee/spec/services/incident_management/issuable_resource_links/create_service_spec.rb'
-- './ee/spec/services/incident_management/issuable_resource_links/destroy_service_spec.rb'
-- './ee/spec/services/incident_management/oncall_rotations/create_service_spec.rb'
-- './ee/spec/services/incident_management/oncall_rotations/destroy_service_spec.rb'
-- './ee/spec/services/incident_management/oncall_rotations/edit_service_spec.rb'
-- './ee/spec/services/incident_management/oncall_rotations/remove_participant_service_spec.rb'
-- './ee/spec/services/incident_management/oncall_rotations/remove_participants_service_spec.rb'
-- './ee/spec/services/incident_management/oncall_schedules/create_service_spec.rb'
-- './ee/spec/services/incident_management/oncall_schedules/destroy_service_spec.rb'
-- './ee/spec/services/incident_management/oncall_schedules/update_service_spec.rb'
-- './ee/spec/services/incident_management/oncall_shifts/read_service_spec.rb'
-- './ee/spec/services/incident_management/pending_escalations/create_service_spec.rb'
-- './ee/spec/services/incident_management/pending_escalations/process_service_spec.rb'
- './ee/spec/services/issuable/destroy_label_links_service_spec.rb'
- './ee/spec/services/issue_feature_flags/list_service_spec.rb'
- './ee/spec/services/issues/build_service_spec.rb'
@@ -3233,13 +3108,6 @@
- './ee/spec/services/software_license_policies/create_service_spec.rb'
- './ee/spec/services/software_license_policies/update_service_spec.rb'
- './ee/spec/services/start_pull_mirroring_service_spec.rb'
-- './ee/spec/services/status_page/mark_for_publication_service_spec.rb'
-- './ee/spec/services/status_page/publish_attachments_service_spec.rb'
-- './ee/spec/services/status_page/publish_details_service_spec.rb'
-- './ee/spec/services/status_page/publish_list_service_spec.rb'
-- './ee/spec/services/status_page/publish_service_spec.rb'
-- './ee/spec/services/status_page/trigger_publish_service_spec.rb'
-- './ee/spec/services/status_page/unpublish_details_service_spec.rb'
- './ee/spec/services/system_notes/epics_service_spec.rb'
- './ee/spec/services/system_note_service_spec.rb'
- './ee/spec/services/system_notes/escalations_service_spec.rb'
@@ -3512,15 +3380,6 @@
- './ee/spec/workers/group_wikis/git_garbage_collect_worker_spec.rb'
- './ee/spec/workers/historical_data_worker_spec.rb'
- './ee/spec/workers/import_software_licenses_worker_spec.rb'
-- './ee/spec/workers/incident_management/apply_incident_sla_exceeded_label_worker_spec.rb'
-- './ee/spec/workers/incident_management/incident_sla_exceeded_check_worker_spec.rb'
-- './ee/spec/workers/incident_management/oncall_rotations/persist_all_rotations_shifts_job_spec.rb'
-- './ee/spec/workers/incident_management/oncall_rotations/persist_shifts_job_spec.rb'
-- './ee/spec/workers/incident_management/pending_escalations/alert_check_worker_spec.rb'
-- './ee/spec/workers/incident_management/pending_escalations/alert_create_worker_spec.rb'
-- './ee/spec/workers/incident_management/pending_escalations/issue_check_worker_spec.rb'
-- './ee/spec/workers/incident_management/pending_escalations/issue_create_worker_spec.rb'
-- './ee/spec/workers/incident_management/pending_escalations/schedule_check_cron_worker_spec.rb'
- './ee/spec/workers/iterations/cadences/create_iterations_worker_spec.rb'
- './ee/spec/workers/iterations/cadences/schedule_create_iterations_worker_spec.rb'
- './ee/spec/workers/iterations/roll_over_issues_worker_spec.rb'
@@ -3559,7 +3418,6 @@
- './ee/spec/workers/security/sync_scan_policies_worker_spec.rb'
- './ee/spec/workers/security/track_secure_scans_worker_spec.rb'
- './ee/spec/workers/set_user_status_based_on_user_cap_setting_worker_spec.rb'
-- './ee/spec/workers/status_page/publish_worker_spec.rb'
- './ee/spec/workers/store_security_reports_worker_spec.rb'
- './ee/spec/workers/sync_seat_link_request_worker_spec.rb'
- './ee/spec/workers/sync_seat_link_worker_spec.rb'
@@ -3787,7 +3645,6 @@
- './spec/controllers/projects/hooks_controller_spec.rb'
- './spec/controllers/projects/import/jira_controller_spec.rb'
- './spec/controllers/projects/imports_controller_spec.rb'
-- './spec/controllers/projects/incidents_controller_spec.rb'
- './spec/controllers/projects/issue_links_controller_spec.rb'
- './spec/controllers/projects/issues_controller_spec.rb'
- './spec/controllers/projects/jobs_controller_spec.rb'
@@ -4081,12 +3938,6 @@
- './spec/features/ide/user_commits_changes_spec.rb'
- './spec/features/ide/user_opens_merge_request_spec.rb'
- './spec/features/import/manifest_import_spec.rb'
-- './spec/features/incidents/incident_details_spec.rb'
-- './spec/features/incidents/incidents_list_spec.rb'
-- './spec/features/incidents/incident_timeline_events_spec.rb'
-- './spec/features/incidents/user_creates_new_incident_spec.rb'
-- './spec/features/incidents/user_filters_incidents_by_status_spec.rb'
-- './spec/features/incidents/user_searches_incidents_spec.rb'
- './spec/features/invites_spec.rb'
- './spec/features/issuables/issuable_list_spec.rb'
- './spec/features/issuables/markdown_references/internal_references_spec.rb'
@@ -4114,7 +3965,6 @@
- './spec/features/issues/form_spec.rb'
- './spec/features/issues/gfm_autocomplete_spec.rb'
- './spec/features/issues/group_label_sidebar_spec.rb'
-- './spec/features/issues/incident_issue_spec.rb'
- './spec/features/issues/issue_detail_spec.rb'
- './spec/features/issues/issue_header_spec.rb'
- './spec/features/issues/issue_sidebar_spec.rb'
@@ -4408,7 +4258,6 @@
- './spec/features/projects/integrations/user_activates_assembla_spec.rb'
- './spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb'
- './spec/features/projects/integrations/user_activates_emails_on_push_spec.rb'
-- './spec/features/projects/integrations/user_activates_flowdock_spec.rb'
- './spec/features/projects/integrations/user_activates_irker_spec.rb'
- './spec/features/projects/integrations/user_activates_issue_tracker_spec.rb'
- './spec/features/projects/integrations/user_activates_jetbrains_teamcity_ci_spec.rb'
@@ -4730,7 +4579,6 @@
- './spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb'
- './spec/finders/groups/projects_requiring_authorizations_refresh/on_transfer_finder_spec.rb'
- './spec/finders/groups/user_groups_finder_spec.rb'
-- './spec/finders/incident_management/timeline_events_finder_spec.rb'
- './spec/finders/issuables/crm_contact_filter_spec.rb'
- './spec/finders/issuables/crm_organization_filter_spec.rb'
- './spec/finders/issues_finder_spec.rb'
@@ -4907,10 +4755,6 @@
- './spec/graphql/mutations/discussions/toggle_resolve_spec.rb'
- './spec/graphql/mutations/environments/canary_ingress/update_spec.rb'
- './spec/graphql/mutations/groups/update_spec.rb'
-- './spec/graphql/mutations/incident_management/timeline_event/create_spec.rb'
-- './spec/graphql/mutations/incident_management/timeline_event/destroy_spec.rb'
-- './spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb'
-- './spec/graphql/mutations/incident_management/timeline_event/update_spec.rb'
- './spec/graphql/mutations/issues/create_spec.rb'
- './spec/graphql/mutations/issues/move_spec.rb'
- './spec/graphql/mutations/issues/set_assignees_spec.rb'
@@ -5023,7 +4867,6 @@
- './spec/graphql/resolvers/group_packages_resolver_spec.rb'
- './spec/graphql/resolvers/group_resolver_spec.rb'
- './spec/graphql/resolvers/groups_resolver_spec.rb'
-- './spec/graphql/resolvers/incident_management/timeline_events_resolver_spec.rb'
- './spec/graphql/resolvers/issues_resolver_spec.rb'
- './spec/graphql/resolvers/issue_status_counts_resolver_spec.rb'
- './spec/graphql/resolvers/kas/agent_configurations_resolver_spec.rb'
@@ -5218,8 +5061,6 @@
- './spec/graphql/types/group_member_relation_enum_spec.rb'
- './spec/graphql/types/group_member_type_spec.rb'
- './spec/graphql/types/group_type_spec.rb'
-- './spec/graphql/types/incident_management/escalation_status_enum_spec.rb'
-- './spec/graphql/types/incident_management/timeline_event_type_spec.rb'
- './spec/graphql/types/invitation_interface_spec.rb'
- './spec/graphql/types/issuable_searchable_field_enum_spec.rb'
- './spec/graphql/types/issuable_severity_enum_spec.rb'
@@ -5470,7 +5311,6 @@
- './spec/helpers/projects/cluster_agents_helper_spec.rb'
- './spec/helpers/projects/error_tracking_helper_spec.rb'
- './spec/helpers/projects_helper_spec.rb'
-- './spec/helpers/projects/incidents_helper_spec.rb'
- './spec/helpers/projects/pipeline_helper_spec.rb'
- './spec/helpers/projects/project_members_helper_spec.rb'
- './spec/helpers/projects/security/configuration_helper_spec.rb'
@@ -5739,7 +5579,6 @@
- './spec/lib/banzai/pipeline/emoji_pipeline_spec.rb'
- './spec/lib/banzai/pipeline/full_pipeline_spec.rb'
- './spec/lib/banzai/pipeline/gfm_pipeline_spec.rb'
-- './spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb'
- './spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb'
- './spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb'
- './spec/lib/banzai/pipeline/post_process_pipeline_spec.rb'
@@ -7118,7 +6957,6 @@
- './spec/lib/gitlab/import/set_async_jid_spec.rb'
- './spec/lib/gitlab/import_sources_spec.rb'
- './spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb'
-- './spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb'
- './spec/lib/gitlab/incoming_email_spec.rb'
- './spec/lib/gitlab/insecure_key_fingerprint_spec.rb'
- './spec/lib/gitlab/instrumentation_helper_spec.rb'
@@ -7598,7 +7436,6 @@
- './spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb'
- './spec/lib/gitlab/tracking/destinations/snowplow_spec.rb'
- './spec/lib/gitlab/tracking/event_definition_spec.rb'
-- './spec/lib/gitlab/tracking/incident_management_spec.rb'
- './spec/lib/gitlab/tracking/snowplow_schema_validation_spec.rb'
- './spec/lib/gitlab/tracking_spec.rb'
- './spec/lib/gitlab/tracking/standard_context_spec.rb'
@@ -7902,7 +7739,6 @@
- './spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb'
- './spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb'
- './spec/migrations/20211210140629_encrypt_static_object_token_spec.rb'
-- './spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb'
- './spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb'
- './spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb'
- './spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb'
@@ -7959,7 +7795,6 @@
- './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/20220629184402_unset_escalation_policies_for_alert_incidents_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'
@@ -8421,9 +8256,6 @@
- './spec/models/identity_spec.rb'
- './spec/models/import_export_upload_spec.rb'
- './spec/models/import_failure_spec.rb'
-- './spec/models/incident_management/issuable_escalation_status_spec.rb'
-- './spec/models/incident_management/project_incident_management_setting_spec.rb'
-- './spec/models/incident_management/timeline_event_spec.rb'
- './spec/models/instance_configuration_spec.rb'
- './spec/models/instance_metadata/kas_spec.rb'
- './spec/models/instance_metadata_spec.rb'
@@ -8455,7 +8287,6 @@
- './spec/models/integrations/ewm_spec.rb'
- './spec/models/integrations/external_wiki_spec.rb'
- './spec/models/integrations/field_spec.rb'
-- './spec/models/integrations/flowdock_spec.rb'
- './spec/models/integrations/hangouts_chat_spec.rb'
- './spec/models/integrations/harbor_spec.rb'
- './spec/models/integrations/irker_spec.rb'
@@ -8795,7 +8626,6 @@
- './spec/policies/group_member_policy_spec.rb'
- './spec/policies/group_policy_spec.rb'
- './spec/policies/identity_provider_policy_spec.rb'
-- './spec/policies/incident_management/timeline_event_policy_spec.rb'
- './spec/policies/instance_metadata_policy_spec.rb'
- './spec/policies/integration_policy_spec.rb'
- './spec/policies/issuable_policy_spec.rb'
@@ -9067,10 +8897,6 @@
- './spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb'
- './spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb'
- './spec/requests/api/graphql/mutations/groups/update_spec.rb'
-- './spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb'
-- './spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb'
-- './spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb'
-- './spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb'
- './spec/requests/api/graphql/mutations/issues/create_spec.rb'
- './spec/requests/api/graphql/mutations/issues/move_spec.rb'
- './spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb'
@@ -9164,7 +8990,6 @@
- './spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb'
- './spec/requests/api/graphql/project/fork_targets_spec.rb'
- './spec/requests/api/graphql/project/grafana_integration_spec.rb'
-- './spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb'
- './spec/requests/api/graphql/project/issue/design_collection/version_spec.rb'
- './spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb'
- './spec/requests/api/graphql/project/issue/designs/designs_spec.rb'
@@ -9395,7 +9220,6 @@
- './spec/requests/projects/harbor/artifacts_controller_spec.rb'
- './spec/requests/projects/harbor/repositories_controller_spec.rb'
- './spec/requests/projects/harbor/tags_controller_spec.rb'
-- './spec/requests/projects/incident_management/pagerduty_incidents_spec.rb'
- './spec/requests/projects/integrations/shimos_controller_spec.rb'
- './spec/requests/projects/issue_links_controller_spec.rb'
- './spec/requests/projects/issues_controller_spec.rb'
@@ -9981,15 +9805,6 @@
- './spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb'
- './spec/services/import/prepare_service_spec.rb'
- './spec/services/import/validate_remote_git_endpoint_service_spec.rb'
-- './spec/services/incident_management/incidents/create_service_spec.rb'
-- './spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb'
-- './spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb'
-- './spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb'
-- './spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb'
-- './spec/services/incident_management/pager_duty/process_webhook_service_spec.rb'
-- './spec/services/incident_management/timeline_events/create_service_spec.rb'
-- './spec/services/incident_management/timeline_events/destroy_service_spec.rb'
-- './spec/services/incident_management/timeline_events/update_service_spec.rb'
- './spec/services/integrations/propagate_service_spec.rb'
- './spec/services/integrations/test/project_service_spec.rb'
- './spec/services/issuable/bulk_update_service_spec.rb'
@@ -10379,8 +10194,6 @@
- './spec/services/system_notes/commit_service_spec.rb'
- './spec/services/system_notes/design_management_service_spec.rb'
- './spec/services/system_note_service_spec.rb'
-- './spec/services/system_notes/incident_service_spec.rb'
-- './spec/services/system_notes/incidents_service_spec.rb'
- './spec/services/system_notes/issuables_service_spec.rb'
- './spec/services/system_notes/merge_requests_service_spec.rb'
- './spec/services/system_notes/time_tracking_service_spec.rb'
@@ -10599,7 +10412,6 @@
- './spec/uploaders/records_uploads_spec.rb'
- './spec/uploaders/terraform/state_uploader_spec.rb'
- './spec/uploaders/uploader_helper_spec.rb'
-- './spec/uploaders/workers/object_storage/background_move_worker_spec.rb'
- './spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb'
- './spec/validators/addressable_url_validator_spec.rb'
- './spec/validators/any_field_validator_spec.rb'
@@ -10945,10 +10757,6 @@
- './spec/workers/hashed_storage/project_rollback_worker_spec.rb'
- './spec/workers/hashed_storage/rollbacker_worker_spec.rb'
- './spec/workers/import_issues_csv_worker_spec.rb'
-- './spec/workers/incident_management/add_severity_system_note_worker_spec.rb'
-- './spec/workers/incident_management/close_incident_worker_spec.rb'
-- './spec/workers/incident_management/pager_duty/process_incident_worker_spec.rb'
-- './spec/workers/incident_management/process_alert_worker_v2_spec.rb'
- './spec/workers/integrations/create_external_cross_reference_worker_spec.rb'
- './spec/workers/integrations/execute_worker_spec.rb'
- './spec/workers/integrations/irker_worker_spec.rb'
diff --git a/spec/support/shared_contexts/disable_user_tracking.rb b/spec/support/shared_contexts/disable_user_tracking.rb
new file mode 100644
index 00000000000..e6689c41d4a
--- /dev/null
+++ b/spec/support/shared_contexts/disable_user_tracking.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'when user tracking is disabled' do
+ before do
+ # rubocop:disable RSpec/AnyInstanceOf
+ allow_any_instance_of(User).to receive(:update_tracked_fields!)
+ allow_any_instance_of(Users::ActivityService).to receive(:execute)
+ # rubocop:enable RSpec/AnyInstanceOf
+ end
+end
diff --git a/spec/support/shared_contexts/email_shared_context.rb b/spec/support/shared_contexts/email_shared_context.rb
index 086cdf50e9d..12d4af5170b 100644
--- a/spec/support/shared_contexts/email_shared_context.rb
+++ b/spec/support/shared_contexts/email_shared_context.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_context :email_shared_context do
+RSpec.shared_context 'email shared context' do
let(:mail_key) { '59d8df8370b7e95c5a49fbf86aeb2c93' }
let(:receiver) { Gitlab::Email::Receiver.new(email_raw) }
let(:markdown) { '![image](uploads/image.png)' }
@@ -27,7 +27,7 @@ def service_desk_fixture(path, slug: nil, key: 'mykey')
fixture_file(path).gsub('project_slug', slug).gsub('project_key', key)
end
-RSpec.shared_examples :reply_processing_shared_examples do
+RSpec.shared_examples 'reply processing shared examples' do
context 'when the user could not be found' do
before do
user.destroy!
@@ -49,7 +49,7 @@ RSpec.shared_examples :reply_processing_shared_examples do
end
end
-RSpec.shared_examples :checks_permissions_on_noteable_examples do
+RSpec.shared_examples 'checks permissions on noteable examples' do
context 'when user has access' do
before do
project.add_reporter(user)
@@ -67,7 +67,7 @@ RSpec.shared_examples :checks_permissions_on_noteable_examples do
end
end
-RSpec.shared_examples :note_handler_shared_examples do |forwardable|
+RSpec.shared_examples 'note handler shared examples' do |forwardable|
context 'when the noteable could not be found' do
before do
noteable.destroy!
@@ -157,7 +157,7 @@ RSpec.shared_examples :note_handler_shared_examples do |forwardable|
noteable.update_attribute(:discussion_locked, true)
end
- it_behaves_like :checks_permissions_on_noteable_examples
+ it_behaves_like 'checks permissions on noteable examples'
end
context 'when everything is fine' do
diff --git a/spec/support/shared_contexts/models/ci/job_token_scope.rb b/spec/support/shared_contexts/models/ci/job_token_scope.rb
new file mode 100644
index 00000000000..51f671b139d
--- /dev/null
+++ b/spec/support/shared_contexts/models/ci/job_token_scope.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'with scoped projects' do
+ let_it_be(:inbound_scoped_project) { create_scoped_project(source_project, direction: :inbound) }
+ let_it_be(:outbound_scoped_project) { create_scoped_project(source_project, direction: :outbound) }
+ let_it_be(:unscoped_project1) { create(:project) }
+ let_it_be(:unscoped_project2) { create(:project) }
+
+ let_it_be(:link_out_of_scope) { create(:ci_job_token_project_scope_link, target_project: unscoped_project1) }
+
+ def create_scoped_project(source_project, direction:)
+ create(:project).tap do |scoped_project|
+ create(
+ :ci_job_token_project_scope_link,
+ source_project: source_project,
+ target_project: scoped_project,
+ direction: direction
+ )
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index a6226fe903b..f6ac98c7669 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -14,7 +14,7 @@ RSpec.shared_context 'GroupPolicy context' do
%i[
read_group read_counts
read_label read_issue_board_list read_milestone read_issue_board
- ]
+ ]
end
let(:guest_permissions) do
@@ -22,32 +22,32 @@ RSpec.shared_context 'GroupPolicy context' do
read_label read_group upload_file read_namespace read_group_activity
read_group_issues read_group_boards read_group_labels read_group_milestones
read_group_merge_requests
- ]
+ ]
end
let(:reporter_permissions) do
%i[
- admin_label
- admin_milestone
- admin_issue_board
- read_container_image
- read_harbor_registry
- read_metrics_dashboard_annotation
- read_prometheus
- read_crm_contact
- read_crm_organization
- ]
+ admin_label
+ admin_milestone
+ admin_issue_board
+ read_container_image
+ read_harbor_registry
+ read_metrics_dashboard_annotation
+ read_prometheus
+ read_crm_contact
+ read_crm_organization
+ ]
end
let(:developer_permissions) do
%i[
- create_metrics_dashboard_annotation
- delete_metrics_dashboard_annotation
- update_metrics_dashboard_annotation
- create_custom_emoji
- create_package
- read_cluster
- ]
+ create_metrics_dashboard_annotation
+ delete_metrics_dashboard_annotation
+ update_metrics_dashboard_annotation
+ create_custom_emoji
+ create_package
+ read_cluster
+ ]
end
let(:maintainer_permissions) do
diff --git a/spec/support/shared_contexts/rack_attack_shared_context.rb b/spec/support/shared_contexts/rack_attack_shared_context.rb
index e7b2ee76c3c..12625ead72b 100644
--- a/spec/support/shared_contexts/rack_attack_shared_context.rb
+++ b/spec/support/shared_contexts/rack_attack_shared_context.rb
@@ -6,7 +6,7 @@ RSpec.shared_context 'rack attack cache store' do
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
# Make time-dependent tests deterministic
- Timecop.freeze { example.run }
+ freeze_time { example.run }
Rack::Attack.cache.store = Rails.cache
end
diff --git a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
index 3974338238a..7c37e5189f1 100644
--- a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
@@ -28,6 +28,10 @@ RSpec.shared_context 'conan api setup' do
)
end
+ let(:snowplow_gitlab_standard_context) do
+ { user: user, project: project, namespace: project.namespace, property: 'i_package_conan_user' }
+ end
+
before do
project.add_developer(user)
allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
diff --git a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
index 89f290d8d68..1e50505162d 100644
--- a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
@@ -17,6 +17,7 @@ RSpec.shared_context 'npm api setup' do
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let(:package_name) { package.name }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_npm_user' } }
before do
# create a duplicated package without triggering model validation errors
diff --git a/spec/support/shared_contexts/rubocop_default_rspec_language_config_context.rb b/spec/support/shared_contexts/rubocop_default_rspec_language_config_context.rb
deleted file mode 100644
index a207c6ae9d1..00000000000
--- a/spec/support/shared_contexts/rubocop_default_rspec_language_config_context.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-# From https://github.com/rubocop/rubocop-rspec/blob/master/spec/shared/default_rspec_language_config_context.rb
-# This can be removed once we have https://github.com/rubocop/rubocop-rspec/pull/1377
-
-RSpec.shared_context 'with default RSpec/Language config' do
- include_context 'config'
-
- # Deep duplication is needed to prevent config leakage between examples
- let(:other_cops) do
- default_language = RuboCop::ConfigLoader
- .default_configuration['RSpec']['Language']
- default_include = RuboCop::ConfigLoader
- .default_configuration['RSpec']['Include']
- { 'RSpec' =>
- {
- 'Include' => default_include,
- 'Language' => deep_dup(default_language)
- } }
- end
-
- def deep_dup(object)
- case object
- when Array
- object.map { |item| deep_dup(item) }
- when Hash
- object.transform_values { |value| deep_dup(value) }
- else
- object # only collections undergo modifications and need duping
- end
- end
-end
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 e26b8cd8b37..7db479bcfd2 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
@@ -23,7 +23,7 @@ RSpec.shared_context 'container repository delete tags service shared context' d
end
def stub_delete_reference_requests(tags)
- tags = Array.wrap(tags).to_h { |tag| [tag, 200] } unless tags.is_a?(Hash)
+ 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}")
diff --git a/spec/support/shared_examples/boards/destroy_service_shared_examples.rb b/spec/support/shared_examples/boards/destroy_service_shared_examples.rb
index b1cb58a736f..578ed0e6260 100644
--- a/spec/support/shared_examples/boards/destroy_service_shared_examples.rb
+++ b/spec/support/shared_examples/boards/destroy_service_shared_examples.rb
@@ -15,7 +15,7 @@ RSpec.shared_examples 'board destroy service' do
expect do
expect(service.execute(board)).to be_success
- end.to change(boards, :count).by(-1)
+ end.to change { boards.count }.by(-1)
end
end
@@ -23,7 +23,7 @@ RSpec.shared_examples 'board destroy service' do
it 'does remove board' do
expect do
service.execute(board)
- end.to change(boards, :count).by(-1)
+ end.to change { boards.count }.by(-1)
end
end
end
diff --git a/spec/support/shared_examples/ci/log_downstream_pipeline_shared_examples.rb b/spec/support/shared_examples/ci/log_downstream_pipeline_shared_examples.rb
index db724dcfe99..0c3b9ec4151 100644
--- a/spec/support/shared_examples/ci/log_downstream_pipeline_shared_examples.rb
+++ b/spec/support/shared_examples/ci/log_downstream_pipeline_shared_examples.rb
@@ -13,10 +13,8 @@ RSpec.shared_examples 'logs downstream pipeline creation' do
end
it 'logs details' do
- pipeline = nil
-
log_entry = record_downstream_pipeline_logs do
- pipeline = subject
+ downstream_pipeline
end
expect(log_entry).to be_present
@@ -24,7 +22,7 @@ RSpec.shared_examples 'logs downstream pipeline creation' do
message: "downstream pipeline created",
class: described_class.name,
root_pipeline_id: expected_root_pipeline.id,
- downstream_pipeline_id: pipeline.id,
+ downstream_pipeline_id: downstream_pipeline.id,
downstream_pipeline_relationship: expected_downstream_relationship,
hierarchy_size: expected_hierarchy_size,
root_pipeline_plan: expected_root_pipeline.project.actual_plan_name,
diff --git a/spec/support/shared_examples/ci/retryable_shared_examples.rb b/spec/support/shared_examples/ci/retryable_shared_examples.rb
index 4622dbe4e31..dc34ea8bb3c 100644
--- a/spec/support/shared_examples/ci/retryable_shared_examples.rb
+++ b/spec/support/shared_examples/ci/retryable_shared_examples.rb
@@ -10,7 +10,7 @@ RSpec.shared_examples 'a retryable job' do
describe '#set_enqueue_immediately!' do
it 'changes #enqueue_immediately? to true' do
expect { subject.set_enqueue_immediately! }
- .to change(subject, :enqueue_immediately?).from(false).to(true)
+ .to change { subject.enqueue_immediately? }.from(false).to(true)
end
end
end
diff --git a/spec/support/shared_examples/ci/stuck_builds_shared_examples.rb b/spec/support/shared_examples/ci/stuck_builds_shared_examples.rb
index 4fcea18393c..dd0a57c6b6d 100644
--- a/spec/support/shared_examples/ci/stuck_builds_shared_examples.rb
+++ b/spec/support/shared_examples/ci/stuck_builds_shared_examples.rb
@@ -30,6 +30,6 @@ end
RSpec.shared_examples 'job is unchanged' do
it 'does not change status' do
- expect { service.execute }.not_to change(job, :status)
+ expect { service.execute }.not_to change { job.status }
end
end
diff --git a/spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb b/spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb
index 710aa333dec..420973b6882 100644
--- a/spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb
@@ -9,7 +9,7 @@ RSpec.shared_examples 'Web hook destroyer' do
delete :destroy, params: params
expect(response).to have_gitlab_http_status(:found)
- expect(flash[:notice]).to eq("#{hook.model_name.human} was deleted")
+ expect(flash[:notice]).to eq('Webhook was deleted')
end
it 'displays a message about async delete', :aggregate_failures do
@@ -20,7 +20,7 @@ RSpec.shared_examples 'Web hook destroyer' do
delete :destroy, params: params
expect(response).to have_gitlab_http_status(:found)
- expect(flash[:notice]).to eq("#{hook.model_name.human} was scheduled for deletion")
+ expect(flash[:notice]).to eq('Webhook was scheduled for deletion')
end
it 'displays an error if deletion failed', :aggregate_failures do
diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
index bbbe93a644f..5506b05ca55 100644
--- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
@@ -356,7 +356,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).and_return(double(execute: project))
- expect { post :create, params: { target_namespace: provider_repo[:name] }, format: :json }.to change(Namespace, :count).by(1)
+ expect { post :create, params: { target_namespace: provider_repo[:name] }, format: :json }.to change { Namespace.count }.by(1)
end
it "takes the new namespace" do
@@ -377,7 +377,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).and_return(double(execute: project))
- expect { post :create, format: :json }.not_to change(Namespace, :count)
+ expect { post :create, format: :json }.not_to change { Namespace.count }
end
it "takes the current user's namespace" do
diff --git a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb
index 6749ebd471f..7e99066110d 100644
--- a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb
+++ b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb
@@ -16,12 +16,14 @@
RSpec.shared_examples 'Snowplow event tracking' do |overrides: {}|
let(:extra) { {} }
- it 'is not emitted if FF is disabled' do
- stub_feature_flags(feature_flag_name => false)
+ if try(:feature_flag_name)
+ it 'is not emitted if FF is disabled' do
+ stub_feature_flags(feature_flag_name => false)
- subject
+ subject
- expect_no_snowplow_event(category: category, action: action)
+ expect_no_snowplow_event(category: category, action: action)
+ end
end
it 'is emitted' do
diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb
index 34632993cf0..d979683cce7 100644
--- a/spec/support/shared_examples/controllers/variables_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb
@@ -19,13 +19,15 @@ RSpec.shared_examples 'PATCH #update updates variables' do
{ id: variable.id,
key: variable.key,
secret_value: variable.value,
- protected: variable.protected?.to_s }
+ protected: variable.protected?.to_s,
+ raw: (!variable.raw?).to_s }
end
let(:new_variable_attributes) do
{ key: 'new_key',
secret_value: 'dummy_value',
- protected: 'false' }
+ protected: 'false',
+ raw: 'true' }
end
let(:variables_scope) { owner.variables }
@@ -86,7 +88,13 @@ RSpec.shared_examples 'PATCH #update updates variables' do
end
it 'updates the existing variable' do
- expect { subject }.to change { variable.reload.value }.to('other_value')
+ old_raw = variable.raw
+
+ subject
+
+ variable.reload
+ expect(variable.value).to eq('other_value')
+ expect(variable.raw?).not_to be(old_raw)
end
it 'creates the new variable' do
diff --git a/spec/support/shared_examples/csp.rb b/spec/support/shared_examples/csp.rb
index 91242ae9f37..725d0a832c2 100644
--- a/spec/support/shared_examples/csp.rb
+++ b/spec/support/shared_examples/csp.rb
@@ -29,7 +29,8 @@ RSpec.shared_examples 'setting CSP' do |rule_name|
context 'when feature is enabled' do
it "appends to #{rule_name}" do
- is_expected.to eql("#{rule_name} #{default_csp_values} #{allowlisted_url}")
+ is_expected.to include("#{rule_name} #{default_csp_values}")
+ is_expected.to include(allowlisted_url)
end
end
@@ -37,7 +38,7 @@ RSpec.shared_examples 'setting CSP' do |rule_name|
include_context 'disable feature'
it "keeps original #{rule_name}" do
- is_expected.to eql("#{rule_name} #{default_csp_values}")
+ is_expected.to include("#{rule_name} #{default_csp_values}")
end
end
end
@@ -47,7 +48,8 @@ RSpec.shared_examples 'setting CSP' do |rule_name|
context 'when feature is enabled' do
it "uses default-src values in #{rule_name}" do
- is_expected.to eql("default-src #{default_csp_values}; #{rule_name} #{default_csp_values} #{allowlisted_url}")
+ is_expected.to include("default-src #{default_csp_values}")
+ is_expected.to include(allowlisted_url)
end
end
@@ -55,7 +57,7 @@ RSpec.shared_examples 'setting CSP' do |rule_name|
include_context 'disable feature'
it "does not add #{rule_name}" do
- is_expected.to eql("default-src #{default_csp_values}")
+ is_expected.to include("default-src #{default_csp_values}")
end
end
end
@@ -65,7 +67,8 @@ RSpec.shared_examples 'setting CSP' do |rule_name|
context 'when feature is enabled' do
it "uses default-src values in #{rule_name}" do
- is_expected.to eql("font-src #{default_csp_values}; #{rule_name} #{allowlisted_url}")
+ is_expected.to include("font-src #{default_csp_values}")
+ is_expected.not_to include("#{rule_name} #{default_csp_values}")
end
end
@@ -73,7 +76,7 @@ RSpec.shared_examples 'setting CSP' do |rule_name|
include_context 'disable feature'
it "does not add #{rule_name}" do
- is_expected.to eql("font-src #{default_csp_values}")
+ is_expected.to include("font-src #{default_csp_values}")
end
end
end
diff --git a/spec/support/shared_examples/features/container_registry_shared_examples.rb b/spec/support/shared_examples/features/container_registry_shared_examples.rb
index 784f82fdda1..06b2b8c621c 100644
--- a/spec/support/shared_examples/features/container_registry_shared_examples.rb
+++ b/spec/support/shared_examples/features/container_registry_shared_examples.rb
@@ -7,19 +7,3 @@ RSpec.shared_examples 'handling feature network errors with the container regist
expect(page).to have_content 'We are having trouble connecting to the Container Registry'
end
end
-
-RSpec.shared_examples 'rejecting tags destruction for an importing repository on' do |tags: []|
- it 'rejects the tag destruction operation' do
- service = instance_double('Projects::ContainerRepository::DeleteTagsService')
- expect(service).to receive(:execute).with(container_repository) { { status: :error, message: 'repository importing' } }
- expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: tags) { service }
-
- first('[data-testid="additional-actions"]').click
- first('[data-testid="single-delete-button"]').click
- expect(find('.modal .modal-title')).to have_content _('Remove tag')
- find('.modal .modal-footer .btn-danger').click
-
- expect(page).to have_content('Tags temporarily cannot be marked for deletion. Please try again in a few minutes.')
- expect(page).to have_link('More details', href: help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion'))
- end
-end
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
index f01e3c88dad..efdf7513b2d 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -327,7 +327,7 @@ RSpec.shared_examples 'edits content using the content editor' do
end
def dropdown_scroll_top
- evaluate_script("document.querySelector('#{suggestions_dropdown} .gl-new-dropdown-inner').scrollTop")
+ evaluate_script("document.querySelector('#{suggestions_dropdown} .gl-dropdown-inner').scrollTop")
end
end
end
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 2cfe353d5d7..7f31ea8f9be 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -64,7 +64,7 @@ RSpec.shared_examples 'a creatable merge request' do
visit project_new_merge_request_path(source_project)
first('.js-target-project').click
- find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click
+ find('.dropdown-target-project li', text: target_project.full_path).click
wait_for_requests
diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
index 68c0d06e7d0..d6f1efc09fc 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -19,6 +19,8 @@ RSpec.shared_examples 'thread comments for commit and snippet' do |resource_name
find('.js-comment-button').click
+ wait_for_all_requests
+
expect(page).to have_content(comment)
new_comment = all(comments_selector).last
@@ -301,7 +303,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re
if resource_name == 'merge request'
let(:note_id) { find("#{comments_selector} .note:first-child", match: :first)['data-note-id'] }
- let(:reply_id) { find("#{comments_selector} .note:last-of-type", match: :first)['data-note-id'] }
+ let(:reply_id) { all("#{comments_selector} [data-note-id]")[1]['data-note-id'] }
it 'can be replied to after resolving' do
find('button[data-testid="resolve-discussion-button"]').click
diff --git a/spec/support/shared_examples/features/inviting_members_shared_examples.rb b/spec/support/shared_examples/features/inviting_members_shared_examples.rb
index 277ec6a7fa7..2eca2a72997 100644
--- a/spec/support/shared_examples/features/inviting_members_shared_examples.rb
+++ b/spec/support/shared_examples/features/inviting_members_shared_examples.rb
@@ -81,7 +81,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
invite_member(user2.name, role: 'Developer')
- invite_member(user2.name, role: 'Reporter', refresh: false)
+ invite_member(user2.name, role: 'Reporter')
expect(page).not_to have_selector(invite_modal_selector)
@@ -101,7 +101,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
invite_member(email, role: 'Developer')
- invite_member(email, role: 'Reporter', refresh: false)
+ invite_member(email, role: 'Reporter')
expect(page).not_to have_selector(invite_modal_selector)
@@ -127,7 +127,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
it 'adds the user as a member on sub-entity with higher access level', :js do
visit subentity_members_page_path
- invite_member(user2.name, role: role, refresh: false)
+ invite_member(user2.name, role: role)
expect(page).not_to have_selector(invite_modal_selector)
@@ -145,7 +145,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
it 'fails with an error', :js do
visit subentity_members_page_path
- invite_member(user2.name, role: role, refresh: false)
+ invite_member(user2.name, role: role)
invite_modal = page.find(invite_modal_selector)
expect(invite_modal).to have_content "#{user2.name}: Access level should be greater than or equal to " \
@@ -177,7 +177,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
visit subentity_members_page_path
- invite_member([user2.name, user3.name, user4.name, user6.name, user7.name], role: role, refresh: false)
+ invite_member([user2.name, user3.name, user4.name, user6.name, user7.name], role: role)
# we have more than 2 errors, so one will be hidden
invite_modal = page.find(invite_modal_selector)
@@ -266,7 +266,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
it 'only shows the error for an invalid formatted email and does not display other member errors', :js do
visit subentity_members_page_path
- invite_member([user2.name, user3.name, 'bad@email'], role: role, refresh: false)
+ invite_member([user2.name, user3.name, 'bad@email'], role: role)
invite_modal = page.find(invite_modal_selector)
expect(invite_modal).to have_text('email contains an invalid email address')
diff --git a/spec/support/shared_examples/features/reportable_note_shared_examples.rb b/spec/support/shared_examples/features/reportable_note_shared_examples.rb
index 288e1df9b2a..c35f711111b 100644
--- a/spec/support/shared_examples/features/reportable_note_shared_examples.rb
+++ b/spec/support/shared_examples/features/reportable_note_shared_examples.rb
@@ -20,7 +20,7 @@ RSpec.shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- expect(dropdown).to have_link('Report abuse to admin', href: abuse_report_path)
+ expect(dropdown).to have_link('Report abuse to administrator', href: abuse_report_path)
if type == 'issue' || type == 'merge_request'
expect(dropdown).to have_button('Delete comment')
@@ -33,7 +33,7 @@ RSpec.shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- dropdown.click_link('Report abuse to admin')
+ dropdown.click_link('Report abuse to administrator')
expect(find('#user_name')['value']).to match(note.author.username)
expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note))
diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb
index a7bc19da45f..20078243cfb 100644
--- a/spec/support/shared_examples/features/runners_shared_examples.rb
+++ b/spec/support/shared_examples/features/runners_shared_examples.rb
@@ -63,10 +63,13 @@ RSpec.shared_examples 'shows and resets runner registration token' do
end
RSpec.shared_examples 'shows no runners registered' do
- it 'shows counts with 0' do
- expect(page).to have_text "#{s_('Runners|Online')} 0"
- expect(page).to have_text "#{s_('Runners|Offline')} 0"
- expect(page).to have_text "#{s_('Runners|Stale')} 0"
+ it 'shows total count with 0' do
+ expect(find('[data-testid="runner-type-tabs"]')).to have_text "#{s_('Runners|All')} 0"
+
+ # No stats are shown
+ expect(page).not_to have_text s_('Runners|Online')
+ expect(page).not_to have_text s_('Runners|Offline')
+ expect(page).not_to have_text s_('Runners|Stale')
end
it 'shows "no runners" message' do
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 cc74c977064..b73f40ff28c 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
@@ -6,7 +6,7 @@ RSpec.shared_examples 'search timeouts' do |scope|
context 'when search times out' do
before do
allow_next_instance_of(SearchService) do |service|
- allow(service).to receive(:search_objects).and_raise(ActiveRecord::QueryCanceled)
+ allow(service).to receive(:search_results).and_raise(ActiveRecord::QueryCanceled)
end
visit(search_path(search: 'test', scope: scope, **additional_params))
diff --git a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
index 281a70e46c4..95b306fdaaa 100644
--- a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
@@ -17,7 +17,7 @@ RSpec.shared_examples 'labels sidebar widget' do
end
it 'shows labels list in the dropdown' do
- expect(labels_widget.find('.gl-new-dropdown-contents')).to have_selector('li.gl-new-dropdown-item', count: 4)
+ expect(labels_widget.find('.gl-dropdown-contents')).to have_selector('li.gl-dropdown-item', count: 4)
end
it 'adds a label' do
@@ -57,8 +57,8 @@ RSpec.shared_examples 'labels sidebar widget' do
expect(page).to have_css('.labels-fetch-loading')
wait_for_all_requests
- expect(page).to have_css('[data-testid="dropdown-content"] .gl-new-dropdown-item')
- expect(page.all(:css, '[data-testid="dropdown-content"] .gl-new-dropdown-item').length).to eq(1)
+ expect(page).to have_css('[data-testid="dropdown-content"] .gl-dropdown-item')
+ expect(page.all(:css, '[data-testid="dropdown-content"] .gl-dropdown-item').length).to eq(1)
find_field('Search').native.send_keys(:enter)
click_button 'Close'
diff --git a/spec/support/shared_examples/features/sidebar/sidebar_milestone_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_milestone_shared_examples.rb
index da730240e8e..f8982c242f8 100644
--- a/spec/support/shared_examples/features/sidebar/sidebar_milestone_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar/sidebar_milestone_shared_examples.rb
@@ -20,15 +20,15 @@ RSpec.shared_examples 'milestone sidebar widget' do
it 'shows milestones list in the dropdown' do
# 5 milestones + "No milestone" = 6 items
- expect(milestone_widget.find('.gl-new-dropdown-contents')).to have_selector('li.gl-new-dropdown-item', count: 6)
+ expect(milestone_widget.find('.gl-dropdown-contents')).to have_selector('li.gl-dropdown-item', count: 6)
end
it 'shows expired milestone at the bottom of the list and milestone due earliest at the top of the list', :aggregate_failures do
- within(milestone_widget, '.gl-new-dropdown-contents') do
+ within(milestone_widget, '.gl-dropdown-contents') do
expect(page.find('li:last-child')).to have_content milestone_expired.title
[milestone3, milestone2, milestone1, milestone_no_duedate].each_with_index do |m, i|
- expect(page.all('li.gl-new-dropdown-item')[i + 1]).to have_content m.title
+ expect(page.all('li.gl-dropdown-item')[i + 1]).to have_content m.title
end
end
end
diff --git a/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb
index 8d1502bed84..7a3b94ad81d 100644
--- a/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb
@@ -55,7 +55,7 @@ RSpec.shared_examples 'wiki file attachments' do
wait_for_requests
expect(page.find('#wiki_content').value)
- .to match(%r{\!\[dk\]\(uploads/\h{32}/dk\.png\)$})
+ .to match(%r{!\[dk\]\(uploads/\h{32}/dk\.png\)$})
end
it 'the links point to the wiki root url' do
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 8b3a344a841..9d1f05d5543 100644
--- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -109,20 +109,77 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
context 'filtering by release' do
- context 'when the release tag is none' do
+ context 'when filter by none' do
let(:params) { { release_tag: 'none' } }
it 'returns items without releases' do
expect(items).to contain_exactly(item2, item3, item4, item5)
end
+
+ context 'when sort by milestone' do
+ let(:params) { { release_tag: 'none', sort: 'milestone_due_desc' } }
+
+ it 'returns items without any releases' do
+ expect(items).to contain_exactly(item2, item3, item4, item5)
+ end
+ end
+ end
+
+ context 'when filter by any' do
+ let(:params) { { release_tag: 'any' } }
+
+ it 'returns items with any releases' do
+ expect(items).to contain_exactly(item1)
+ end
+
+ context 'when sort by milestone' do
+ let(:params) { { release_tag: 'any', sort: 'milestone_due_desc' } }
+
+ it 'returns items without any releases' do
+ expect(items).to contain_exactly(item1)
+ end
+ end
end
- context 'when the release tag exists' do
+ context 'when filter by a release_tag' do
let(:params) { { project_id: project1.id, release_tag: release.tag } }
- it 'returns the items associated with that release' do
+ it 'returns the items associated with the release tag' do
expect(items).to contain_exactly(item1)
end
+
+ context 'when sort by milestone' do
+ let(:params) { { project_id: project1.id, release_tag: release.tag, sort: 'milestone_due_desc' } }
+
+ it 'returns the items associated with the release tag' do
+ expect(items).to contain_exactly(item1)
+ end
+ end
+ end
+
+ context 'when filter by a negated release_tag' do
+ let_it_be(:another_release) { create(:release, project: project1, tag: 'v2.0.0') }
+ let_it_be(:another_milestone) { create(:milestone, project: project1, releases: [another_release]) }
+ let_it_be(:another_item) do
+ create(factory,
+ project: project1,
+ milestone: another_milestone,
+ title: 'another item')
+ end
+
+ let(:params) { { not: { release_tag: release.tag, project_id: project1.id } } }
+
+ it 'returns the items not associated with the release' do
+ expect(items).to contain_exactly(another_item)
+ end
+
+ context 'when sort by milestone' do
+ let(:params) { { not: { release_tag: release.tag, project_id: project1.id }, sort: 'milestone_due_desc' } }
+
+ it 'returns the items not associated with the release' do
+ expect(items).to contain_exactly(another_item)
+ end
+ end
end
end
@@ -864,12 +921,15 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
context 'filtering by item type' do
let_it_be(:incident_item) { create(factory, issue_type: :incident, project: project1) }
+ let_it_be(:objective) { create(factory, issue_type: :objective, project: project1) }
+ let_it_be(:key_result) { create(factory, issue_type: :key_result, project: project1) }
context 'no type given' do
let(:params) { { issue_types: [] } }
it 'returns all items' do
- expect(items).to contain_exactly(incident_item, item1, item2, item3, item4, item5)
+ expect(items)
+ .to contain_exactly(incident_item, item1, item2, item3, item4, item5, objective, key_result)
end
end
@@ -881,6 +941,22 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
end
+ context 'objective type' do
+ let(:params) { { issue_types: ['objective'] } }
+
+ it 'returns incident items' do
+ expect(items).to contain_exactly(objective)
+ end
+ end
+
+ context 'key_result type' do
+ let(:params) { { issue_types: ['key_result'] } }
+
+ it 'returns incident items' do
+ expect(items).to contain_exactly(key_result)
+ end
+ end
+
context 'item type' do
let(:params) { { issue_types: ['issue'] } }
diff --git a/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb b/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb
index 601a53ed913..f00d6e776ec 100644
--- a/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb
+++ b/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb
@@ -237,8 +237,8 @@ RSpec.shared_examples 'snippet visibility' do
if project.private?
project.add_developer(external) unless member
- else
- member.delete if member
+ elsif member
+ member.delete
end
end
end
diff --git a/spec/support/shared_examples/graphql/label_fields.rb b/spec/support/shared_examples/graphql/label_fields.rb
index 4159e4e03ab..030a2feafcd 100644
--- a/spec/support/shared_examples/graphql/label_fields.rb
+++ b/spec/support/shared_examples/graphql/label_fields.rb
@@ -42,9 +42,7 @@ RSpec.shared_examples 'querying a GraphQL type with labels' do
make_query(
[
query_graphql_field(:label, label_params, all_graphql_fields_for(Label)),
- query_graphql_field(:labels, labels_params, [
- query_graphql_field(:nodes, nil, all_graphql_fields_for(Label))
- ])
+ query_graphql_field(:labels, labels_params, [query_graphql_field(:nodes, nil, all_graphql_fields_for(Label))])
]
)
end
diff --git a/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb
index cd591248ff6..030aca5bd1c 100644
--- a/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb
@@ -7,7 +7,7 @@ require 'spec_helper'
# * Defined expected timeline event via `let(:expected_timeline_event) { instance_double(...) }`
RSpec.shared_examples 'creating an incident timeline event' do
it 'creates a timeline event' do
- expect { resolve }.to change(IncidentManagement::TimelineEvent, :count).by(1)
+ expect { resolve }.to change { IncidentManagement::TimelineEvent.count }.by(1)
end
it 'responds with a timeline event', :aggregate_failures do
diff --git a/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb
index f28348fb945..c6402a89f02 100644
--- a/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb
@@ -1,6 +1,17 @@
# frozen_string_literal: true
RSpec.shared_examples 'issuable supports timelog creation mutation' do
+ let(:mutation_response) { graphql_mutation_response(:timelog_create) }
+ let(:mutation) do
+ variables = {
+ 'time_spent' => time_spent,
+ 'spent_at' => '2022-11-16T12:59:35+0100',
+ 'summary' => 'Test summary',
+ 'issuable_id' => issuable.to_global_id.to_s
+ }
+ graphql_mutation(:timelogCreate, variables)
+ end
+
context 'when the user is anonymous' do
before do
post_graphql_mutation(mutation, current_user: current_user)
@@ -32,13 +43,14 @@ RSpec.shared_examples 'issuable supports timelog creation mutation' do
it 'creates the timelog' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
- end.to change(Timelog, :count).by(1)
+ end.to change { Timelog.count }.by(1)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['errors']).to be_empty
expect(mutation_response['timelog']).to include(
'timeSpent' => 3600,
- 'spentAt' => '2022-07-08T00:00:00Z',
+ # This also checks that the ISO time was converted to UTC
+ 'spentAt' => '2022-11-16T11:59:35Z',
'summary' => 'Test summary'
)
end
@@ -50,10 +62,11 @@ RSpec.shared_examples 'issuable supports timelog creation mutation' do
it 'returns an error' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
- end.to change(Timelog, :count).by(0)
+ end.to change { Timelog.count }.by(0)
expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['errors']).to match_array(['Time spent can\'t be blank'])
+ expect(mutation_response['errors']).to match_array(
+ ['Time spent must be formatted correctly. For example: 1h 30m.'])
expect(mutation_response['timelog']).to be_nil
end
end
@@ -61,6 +74,17 @@ RSpec.shared_examples 'issuable supports timelog creation mutation' do
end
RSpec.shared_examples 'issuable does not support timelog creation mutation' do
+ let(:mutation_response) { graphql_mutation_response(:timelog_create) }
+ let(:mutation) do
+ variables = {
+ 'time_spent' => time_spent,
+ 'spent_at' => '2022-11-16T12:59:35+0100',
+ 'summary' => 'Test summary',
+ 'issuable_id' => issuable.to_global_id.to_s
+ }
+ graphql_mutation(:timelogCreate, variables)
+ end
+
context 'when the user is anonymous' do
before do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb
index 56c2ca22e15..f672ec7f5ac 100644
--- a/spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'update work item description widget' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
work_item.reload
- end.to change(work_item, :description).from(nil).to(new_description)
+ end.to change { work_item.description }.from(nil).to(new_description)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['workItem']['widgets']).to include(
diff --git a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
index bdd4dbfe209..3ff93371c19 100644
--- a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
@@ -20,7 +20,7 @@ RSpec.shared_examples 'a Note mutation when the user does not have permission' d
it_behaves_like 'a Note mutation that does not create a Note'
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']
end
RSpec.shared_examples 'a Note mutation when there are active record validation errors' do |model: Note|
@@ -74,7 +74,7 @@ RSpec.shared_examples 'a Note mutation when there are rate limit validation erro
it_behaves_like 'a Note mutation that does not create a Note'
it_behaves_like 'a mutation that returns top-level errors',
- errors: ['This endpoint has been requested too many times. Try again later.']
+ errors: ['This endpoint has been requested too many times. Try again later.']
context 'when the user is in the allowlist' do
before do
@@ -97,3 +97,55 @@ RSpec.shared_examples 'a Note mutation with confidential notes' do
expect(mutation_response['note']['internal']).to eq(true)
end
end
+
+RSpec.shared_examples 'a Note mutation updates a note successfully' do
+ it 'updates the Note' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(note.reload.note).to eq(updated_body)
+ end
+
+ it 'returns the updated Note' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['note']['body']).to eq(updated_body)
+ end
+end
+
+RSpec.shared_examples 'a Note mutation update with errors' do
+ context 'when there are ActiveRecord validation errors' do
+ let(:params) { { body: '', confidential: true } }
+
+ it_behaves_like 'a mutation that returns errors in the response',
+ errors: ["Note can't be blank", 'Confidential can not be changed for existing notes']
+
+ it 'does not update the Note' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(note.reload.note).to eq(original_body)
+ expect(note.confidential).to be_falsey
+ end
+
+ it 'returns the original Note' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['note']['body']).to eq(original_body)
+ expect(mutation_response['note']['confidential']).to be_falsey
+ end
+ end
+end
+
+RSpec.shared_examples 'a Note mutation update only with quick actions' do
+ context 'when body only contains quick actions' do
+ let(:updated_body) { '/close' }
+
+ it 'returns a nil note and empty errors' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response).to include(
+ 'errors' => [],
+ 'note' => nil
+ )
+ end
+ end
+end
diff --git a/spec/support/shared_examples/initializers/uses_gitlab_url_blocker_shared_examples.rb b/spec/support/shared_examples/initializers/uses_gitlab_url_blocker_shared_examples.rb
index afa495fc9a4..553e9f10b0d 100644
--- a/spec/support/shared_examples/initializers/uses_gitlab_url_blocker_shared_examples.rb
+++ b/spec/support/shared_examples/initializers/uses_gitlab_url_blocker_shared_examples.rb
@@ -152,4 +152,11 @@ RSpec.shared_examples 'a request using Gitlab::UrlBlocker' do
expect(request_stub).to have_been_requested
end
end
+
+ context 'when a non HTTP/HTTPS URL is provided' do
+ it 'raises an error' do
+ expect { make_request('ssh://example.com') }
+ .to raise_error(ArgumentError)
+ end
+ end
end
diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_shared_examples.rb
index 459d4f5cd3e..7141492bd49 100644
--- a/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_shared_examples.rb
@@ -16,20 +16,20 @@ RSpec.shared_examples 'backfill migration for project repositories' do |storage|
projects.create!(name: "foo-#{index}", path: "foo-#{index}", namespace_id: group.id, storage_version: storage_version)
end
- expect { described_class.new.perform(1, projects.last.id) }.to change(project_repositories, :count).by(2)
+ expect { described_class.new.perform(1, projects.last.id) }.to change { project_repositories.count }.by(2)
end
it "does nothing for projects on #{storage} storage that have already a project_repository row" do
projects.create!(id: 1, name: 'foo', path: 'foo', namespace_id: group.id, storage_version: storage_version)
project_repositories.create!(project_id: 1, disk_path: 'phony/foo/bar', shard_id: shard.id)
- expect { described_class.new.perform(1, projects.last.id) }.not_to change(project_repositories, :count)
+ expect { described_class.new.perform(1, projects.last.id) }.not_to change { project_repositories.count }
end
it "does nothing for projects on #{storage == :legacy ? 'hashed' : 'legacy'} storage" do
projects.create!(name: 'foo', path: 'foo', namespace_id: group.id, storage_version: storage == :legacy ? 1 : nil)
- expect { described_class.new.perform(1, projects.last.id) }.not_to change(project_repositories, :count)
+ expect { described_class.new.perform(1, projects.last.id) }.not_to change { project_repositories.count }
end
it 'inserts rows in a single query' do
diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
index 9ffc55f7e7e..d471a758f3e 100644
--- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
+++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
@@ -62,14 +62,14 @@ RSpec.shared_examples 'deployment metrics examples' do
describe '#deployment_frequency' do
subject { stage_summary.fourth[:value] }
- it 'includes the unit: `/day`' do
- expect(stage_summary.fourth[:unit]).to eq _('/day')
- end
-
before do
travel_to(5.days.ago) { create_deployment(project: project) }
end
+ it 'includes the unit: `/day`' do
+ expect(stage_summary.fourth[:unit]).to eq _('/day')
+ end
+
it 'returns 0.0 when there were deploys but the frequency was too low' do
options[:from] = 30.days.ago
diff --git a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb
index a28fefcfc58..286f10a186d 100644
--- a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb
@@ -28,7 +28,7 @@ RSpec.shared_examples 'finalized background migration' do |worker_class|
.new
.select do |scheduled|
scheduled.klass == worker_class.name &&
- scheduled.args.first == job_class_name
+ scheduled.args.first == job_class_name
end
expect(queued.size).to eq(0)
end
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 40deaa27955..16b048ae325 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
@@ -25,10 +25,12 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
end
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) }
- ])
+ 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) }
+ ]
+ )
subject
end
@@ -61,10 +63,12 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
end
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) }
- ])
+ 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) }
+ ]
+ )
subject
end
@@ -101,10 +105,12 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
end
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) }
- ])
+ 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) }
+ ]
+ )
subject
end
@@ -133,11 +139,13 @@ RSpec.shared_examples 'handling all upload parameters conditions' do
end
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) }
- ])
+ 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) }
+ ]
+ )
subject
end
diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb
index 481e11bcf0e..d4802a19202 100644
--- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb
@@ -1,38 +1,5 @@
# frozen_string_literal: true
-RSpec.shared_examples 'a daily tracked issuable event' do
- before do
- stub_application_setting(usage_ping_enabled: true)
- end
-
- def count_unique(date_from: 1.minute.ago, date_to: 1.minute.from_now)
- Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to)
- end
-
- specify do
- aggregate_failures do
- expect(track_action(author: user1)).to be_truthy
- expect(track_action(author: user1)).to be_truthy
- expect(track_action(author: user2)).to be_truthy
- expect(count_unique).to eq(2)
- end
- end
-
- it 'does not track edit actions if author is not present' do
- expect(track_action(author: nil)).to be_nil
- end
-end
-
-RSpec.shared_examples 'does not track when feature flag is disabled' do |feature_flag|
- context "when feature flag #{feature_flag} is disabled" do
- it 'does not track action' do
- stub_feature_flags(feature_flag => false)
-
- expect(track_action(author: user1)).to be_nil
- end
- end
-end
-
RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events for given event params' do
before do
stub_application_setting(usage_ping_enabled: true)
@@ -76,15 +43,27 @@ end
RSpec.shared_examples 'daily tracked issuable snowplow and service ping events with project' do
it_behaves_like 'a daily tracked issuable snowplow and service ping events for given event params' do
+ let(:context) do
+ Gitlab::Tracking::ServicePingContext
+ .new(data_source: :redis_hll, event: event_property)
+ .to_h
+ end
+
let(:track_params) { { project: project } }
- let(:event_params) { track_params.merge(label: event_label, property: event_property, namespace: project.namespace) }
+ let(:event_params) { track_params.merge(label: event_label, property: event_property, namespace: project.namespace, context: [context]) }
end
end
RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events with namespace' do
it_behaves_like 'a daily tracked issuable snowplow and service ping events for given event params' do
+ let(:context) do
+ Gitlab::Tracking::ServicePingContext
+ .new(data_source: :redis_hll, event: event_property)
+ .to_h
+ end
+
let(:track_params) { { namespace: namespace } }
- let(:event_params) { track_params.merge(label: event_label, property: event_property) }
+ let(:event_params) { track_params.merge(label: event_label, property: event_property, context: [context]) }
end
end
diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb
index 919311adc96..b0cbf0b0d65 100644
--- a/spec/support/shared_examples/mailers/notify_shared_examples.rb
+++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb
@@ -75,7 +75,7 @@ RSpec.shared_examples 'a new thread email with reply-by-email enabled' do
aggregate_failures do
is_expected.to have_header('Message-ID', "<#{route_key}@#{host}>")
- is_expected.to have_header('References', /\A<reply\-.*@#{host}>\Z/ )
+ is_expected.to have_header('References', /\A<reply-.*@#{host}>\Z/ )
end
end
end
@@ -91,7 +91,7 @@ RSpec.shared_examples 'a thread answer email with reply-by-email enabled' do
aggregate_failures do
is_expected.to have_header('Message-ID', /\A<.*@#{host}>\Z/)
is_expected.to have_header('In-Reply-To', "<#{route_key}@#{host}>")
- is_expected.to have_header('References', /\A<reply\-.*@#{host}> <#{route_key}@#{host}>\Z/ )
+ is_expected.to have_header('References', /\A<reply-.*@#{host}> <#{route_key}@#{host}>\Z/ )
is_expected.to have_subject(/^Re: /)
end
end
diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
index ef4b08c7865..c07d1552ba2 100644
--- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
+++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
@@ -129,8 +129,8 @@ RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do
expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1, { db_config_name: db_config_name })
expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_cached_count_total".to_sym, 1, { db_config_name: db_config_name }) if record_cached_query
end
- else
- expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1, { db_config_name: db_config_name }) if db_role
+ elsif db_role
+ expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1, { db_config_name: db_config_name })
end
subscriber.sql(event)
diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
index a658d02f09a..a20bb794095 100644
--- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
@@ -6,21 +6,18 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
Gitlab::ApplicationContext.push(feature_category: 'test', caller_id: 'caller')
end
- it 'defines a Redis counter_key' do
- expect(model.counter_key(:counter_name))
- .to eq("project:{#{model.project_id}}:counters:CounterAttributeModel:#{model.id}:counter_name")
- end
-
it 'defines a method to store counters' do
- expect(model.class.counter_attributes.to_a).to eq(counter_attributes)
+ registered_attributes = model.class.counter_attributes.map { |e| e[:attribute] } # rubocop:disable Rails/Pluck
+ expect(registered_attributes).to contain_exactly(*counter_attributes)
end
counter_attributes.each do |attribute|
describe attribute do
- describe '#delayed_increment_counter', :redis do
+ describe '#increment_counter', :redis do
let(:increment) { 10 }
+ let(:counter_key) { model.counter(attribute).key }
- subject { model.delayed_increment_counter(attribute, increment) }
+ subject { model.increment_counter(attribute, increment) }
context 'when attribute is a counter attribute' do
where(:increment) { [10, -3] }
@@ -44,7 +41,7 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
subject
Gitlab::Redis::SharedState.with do |redis|
- counter = redis.get(model.counter_key(attribute))
+ counter = redis.get(counter_key)
expect(counter).to eq(increment.to_s)
end
end
@@ -55,7 +52,7 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
it 'schedules a worker to flush counter increments asynchronously' do
expect(FlushCounterIncrementsWorker).to receive(:perform_in)
- .with(CounterAttribute::WORKER_DELAY, model.class.name, model.id, attribute)
+ .with(Gitlab::Counters::BufferedCounter::WORKER_DELAY, model.class.name, model.id, attribute)
.and_call_original
subject
@@ -73,128 +70,13 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
end
end
end
-
- context 'when attribute is not a counter attribute' do
- it 'raises ArgumentError' do
- expect { model.delayed_increment_counter(:unknown_attribute, 10) }
- .to raise_error(ArgumentError, 'unknown_attribute is not a counter attribute')
- end
- end
- end
- end
- end
-
- describe '.flush_increments_to_database!', :redis do
- let(:incremented_attribute) { counter_attributes.first }
-
- subject { model.flush_increments_to_database!(incremented_attribute) }
-
- it 'obtains an exclusive lease during processing' do
- expect(model)
- .to receive(:in_lock)
- .with(model.counter_lock_key(incremented_attribute), ttl: described_class::WORKER_LOCK_TTL)
- .and_call_original
-
- subject
- end
-
- context 'when there is a counter to flush' do
- before do
- model.delayed_increment_counter(incremented_attribute, 10)
- model.delayed_increment_counter(incremented_attribute, -3)
- end
-
- it 'updates the record and logs it', :aggregate_failures do
- expect(Gitlab::AppLogger).to receive(:info).with(
- hash_including(
- message: 'Acquiring lease for project statistics update',
- attributes: [incremented_attribute]
- )
- )
-
- expect(Gitlab::AppLogger).to receive(:info).with(
- hash_including(
- message: 'Flush counter attribute to database',
- attribute: incremented_attribute,
- project_id: model.project_id,
- increment: 7,
- previous_db_value: 0,
- new_db_value: 7,
- 'correlation_id' => an_instance_of(String),
- 'meta.feature_category' => 'test',
- 'meta.caller_id' => 'caller'
- )
- )
-
- expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(7)
- end
-
- it 'removes the increment entry from Redis' do
- Gitlab::Redis::SharedState.with do |redis|
- key_exists = redis.exists?(model.counter_key(incremented_attribute))
- expect(key_exists).to be_truthy
- end
-
- subject
-
- Gitlab::Redis::SharedState.with do |redis|
- key_exists = redis.exists?(model.counter_key(incremented_attribute))
- expect(key_exists).to be_falsey
- end
- end
- end
-
- context 'when there are no counters to flush' do
- context 'when there are no counters in the relative :flushed key' do
- it 'does not change the record' do
- expect { subject }.not_to change { model.reset.attributes }
- end
- end
-
- # This can be the case where updating counters in the database fails with error
- # and retrying the worker will retry flushing the counters but the main key has
- # disappeared and the increment has been moved to the "<...>:flushed" key.
- context 'when there are counters in the relative :flushed key' do
- before do
- Gitlab::Redis::SharedState.with do |redis|
- redis.incrby(model.counter_flushed_key(incremented_attribute), 10)
- end
- end
-
- it 'updates the record' do
- expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(10)
- end
-
- it 'deletes the relative :flushed key' do
- subject
-
- Gitlab::Redis::SharedState.with do |redis|
- key_exists = redis.exists?(model.counter_flushed_key(incremented_attribute))
- expect(key_exists).to be_falsey
- end
- end
- end
- end
-
- context 'when deleting :flushed key fails' do
- before do
- Gitlab::Redis::SharedState.with do |redis|
- redis.incrby(model.counter_flushed_key(incremented_attribute), 10)
-
- expect(redis).to receive(:del).and_raise('could not delete key')
- end
- end
-
- it 'does a rollback of the counter update' do
- expect { subject }.to raise_error('could not delete key')
-
- expect(model.reset.read_attribute(incremented_attribute)).to eq(0)
end
end
end
describe '#reset_counter!' do
let(:attribute) { counter_attributes.first }
+ let(:counter_key) { model.counter(attribute).key }
before do
model.update!(attribute => 123)
@@ -207,7 +89,7 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
expect { subject }.to change { model.reload.send(attribute) }.from(123).to(0)
Gitlab::Redis::SharedState.with do |redis|
- key_exists = redis.exists?(model.counter_key(attribute))
+ key_exists = redis.exists?(counter_key)
expect(key_exists).to be_falsey
end
end
diff --git a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
index f0581333b28..2e528f7996c 100644
--- a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
@@ -5,14 +5,16 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
let_it_be(:project) { create(:project, :repository, :wiki_repo) }
let_it_be(:integration) { create(factory, branches_to_be_notified: 'all', project: project) }
- before do
- stub_request(:post, integration.webhook)
+ def usage_tracking_key(action)
+ prefix = integration.send(:metrics_key_prefix)
+
+ "#{prefix}_#{action}_notification"
end
it 'uses only known events', :aggregate_failures do
described_class::SUPPORTED_EVENTS_FOR_USAGE_LOG.each do |action|
expect(
- Gitlab::UsageDataCounters::HLLRedisCounter.known_event?("i_ecosystem_slack_service_#{action}_notification")
+ Gitlab::UsageDataCounters::HLLRedisCounter.known_event?(usage_tracking_key(action))
).to be true
end
end
@@ -20,7 +22,9 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
context 'when hook data includes a user object' do
let_it_be(:user) { create_default(:user) }
- shared_examples 'increases the usage data counter' do |event_name|
+ shared_examples 'increases the usage data counter' do |event|
+ let(:event_name) { usage_tracking_key(event) }
+
subject(:execute) { integration.execute(data) }
it 'increases the usage data counter' do
@@ -30,7 +34,7 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
execute
end
- it_behaves_like 'Snowplow event tracking' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { described_class.to_s }
let(:action) { 'perform_integrations_action' }
@@ -47,7 +51,7 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
it 'does not increase the usage data counter' do
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
- .not_to receive(:track_event).with('i_ecosystem_slack_service_pipeline_notification', values: user.id)
+ .not_to receive(:track_event).with(usage_tracking_key(:pipeline), values: user.id)
integration.execute(data)
end
@@ -58,13 +62,13 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
let(:data) { issue.to_hook_data(user) }
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_issue_notification'
+ it_behaves_like 'increases the usage data counter', :issue
end
context 'for push notification' do
let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_push_notification'
+ it_behaves_like 'increases the usage data counter', :push
end
context 'for deployment notification' do
@@ -72,7 +76,7 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.current) }
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_deployment_notification'
+ it_behaves_like 'increases the usage data counter', :deployment
end
context 'for wiki_page notification' do
@@ -88,7 +92,7 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
allow(project.wiki).to receive(:after_wiki_activity)
end
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_wiki_page_notification'
+ it_behaves_like 'increases the usage data counter', :wiki_page
end
context 'for merge_request notification' do
@@ -96,7 +100,7 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
let(:data) { merge_request.to_hook_data(user) }
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_merge_request_notification'
+ it_behaves_like 'increases the usage data counter', :merge_request
end
context 'for note notification' do
@@ -104,7 +108,7 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) }
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_note_notification'
+ it_behaves_like 'increases the usage data counter', :note
end
context 'for tag_push notification' do
@@ -115,7 +119,7 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
Git::TagHooksService.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }).send(:push_data)
end
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_tag_push_notification'
+ it_behaves_like 'increases the usage data counter', :tag_push
end
context 'for confidential note notification' do
@@ -125,7 +129,7 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
let(:data) { Gitlab::DataBuilder::Note.build(confidential_issue_note, user) }
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_confidential_note_notification'
+ it_behaves_like 'increases the usage data counter', :confidential_note
end
context 'for confidential issue notification' do
@@ -133,7 +137,7 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
let(:data) { issue.to_hook_data(user) }
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_confidential_issue_notification'
+ it_behaves_like 'increases the usage data counter', :confidential_issue
end
end
diff --git a/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb b/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb
index ed94a71892d..aedbfe4deb3 100644
--- a/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'sanitizable' do |factory, fields|
- let(:attributes) { fields.to_h { |field| [field, input] } }
+ let(:attributes) { fields.index_with { input } }
it 'includes Sanitizable' do
expect(described_class).to include(Sanitizable)
diff --git a/spec/support/shared_examples/models/concerns/signature_type_shared_examples.rb b/spec/support/shared_examples/models/concerns/signature_type_shared_examples.rb
new file mode 100644
index 00000000000..728855b74f8
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/signature_type_shared_examples.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+METHODS = %i[
+ gpg?
+ ssh?
+ x509?
+].freeze
+
+RSpec.shared_examples 'signature with type checking' do |type|
+ describe 'signature type checkers' do
+ where(:method, :expected) do
+ METHODS.map do |method|
+ [method, method == "#{type}?".to_sym]
+ end
+ end
+
+ with_them do
+ specify { expect(subject.public_send(method)).to eq(expected) }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
index a764d47d7c0..a1269b158a2 100644
--- a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
+++ b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
@@ -65,7 +65,7 @@ RSpec.shared_examples Integrations::HasWebHook do
end
it 'creates or updates a service hook' do
- expect { call }.to change(ServiceHook, :count).by(1)
+ expect { call }.to change { ServiceHook.count }.by(1)
expect(integration.service_hook.url).to eq(hook_url)
integration.service_hook.update!(url: 'http://other.com')
@@ -98,10 +98,10 @@ RSpec.shared_examples Integrations::HasWebHook do
it 'creates the webhook if necessary and executes it' do
expect_next(ServiceHook).to receive(:execute).with(*args)
- expect { call }.to change(ServiceHook, :count).by(1)
+ expect { call }.to change { ServiceHook.count }.by(1)
expect(integration.service_hook).to receive(:execute).with(*args)
- expect { call }.not_to change(ServiceHook, :count)
+ expect { call }.not_to change { ServiceHook.count }
end
it 'raises an error if the service hook could not be saved' do
diff --git a/spec/support/shared_examples/models/label_note_shared_examples.rb b/spec/support/shared_examples/models/label_note_shared_examples.rb
index f61007f57fd..3facd533d7a 100644
--- a/spec/support/shared_examples/models/label_note_shared_examples.rb
+++ b/spec/support/shared_examples/models/label_note_shared_examples.rb
@@ -30,6 +30,8 @@ RSpec.shared_examples 'label note created from events' do
expect(note.noteable).to eq event.issuable
expect(note.note).to be_present
expect(note.note_html).to be_present
+ expect(note.created_at).to eq create_event.created_at
+ expect(note.updated_at).to eq create_event.created_at
end
it 'updates markdown cache if reference is not set yet' do
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index 287b046cbec..f8cff5c5558 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -491,7 +491,7 @@ RSpec.shared_examples_for "bulk member creation" do
:developer,
tasks_to_be_done: %w(issues),
tasks_project_id: task_project.id)
- end.not_to change(MemberTask, :count)
+ end.not_to change { MemberTask.count }
member.reset
expect(member.tasks_to_be_done).to match_array([:code, :ci])
@@ -505,7 +505,7 @@ RSpec.shared_examples_for "bulk member creation" do
:developer,
tasks_to_be_done: %w(issues),
tasks_project_id: task_project.id)
- end.to change(MemberTask, :count).by(1)
+ end.to change { MemberTask.count }.by(1)
member = source.members.find_by(user_id: user1.id)
expect(member.tasks_to_be_done).to match_array([:issues])
diff --git a/spec/support/shared_examples/models/update_highest_role_shared_examples.rb b/spec/support/shared_examples/models/update_highest_role_shared_examples.rb
index 34c4ada1718..1fdd0962fbb 100644
--- a/spec/support/shared_examples/models/update_highest_role_shared_examples.rb
+++ b/spec/support/shared_examples/models/update_highest_role_shared_examples.rb
@@ -25,7 +25,7 @@ RSpec.shared_examples 'update highest role with exclusive lease' do
expect(UpdateHighestRoleWorker).to receive(:perform_in).with(described_class::HIGHEST_ROLE_JOB_DELAY, user_id).and_call_original
- expect { subject }.to change(UpdateHighestRoleWorker.jobs, :size).by(1)
+ expect { subject }.to change { UpdateHighestRoleWorker.jobs.size }.by(1)
end
end
@@ -33,7 +33,7 @@ RSpec.shared_examples 'update highest role with exclusive lease' do
it 'only schedules one job' do
stub_exclusive_lease_taken(lease_key, timeout: described_class::HIGHEST_ROLE_LEASE_TIMEOUT)
- expect { subject }.not_to change(UpdateHighestRoleWorker.jobs, :size)
+ expect { subject }.not_to change { UpdateHighestRoleWorker.jobs.size }
end
end
end
diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
index b81bd514d0a..eb742921d35 100644
--- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
+++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
@@ -15,7 +15,7 @@ RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute|
def read_pending_increment
Gitlab::Redis::SharedState.with do |redis|
- key = project.statistics.counter_key(project_statistics_name)
+ key = project.statistics.counter(project_statistics_name).key
redis.get(key).to_i
end
end
@@ -25,7 +25,7 @@ RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute|
def expect_flush_counter_increments_worker_performed
expect(FlushCounterIncrementsWorker)
.to receive(:perform_in)
- .with(CounterAttribute::WORKER_DELAY, project.statistics.class.name, project.statistics.id, project_statistics_name)
+ .with(Gitlab::Counters::BufferedCounter::WORKER_DELAY, project.statistics.class.name, project.statistics.id, project_statistics_name)
yield
diff --git a/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb b/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb
index e86f1e77447..d6071b20dca 100644
--- a/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb
+++ b/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb
@@ -9,9 +9,9 @@ RSpec.shared_examples 'model with Debian distributions' do
it 'removes distribution files on removal' do
distribution_file_paths = distributions.map do |distribution|
[distribution.file.path] +
- distribution.component_files.map do |component_file|
- component_file.file.path
- end
+ distribution.component_files.map do |component_file|
+ component_file.file.path
+ end
end.flatten
expect { subject.destroy! }
diff --git a/spec/support/shared_examples/observability/csp_shared_examples.rb b/spec/support/shared_examples/observability/csp_shared_examples.rb
new file mode 100644
index 00000000000..868d7023d14
--- /dev/null
+++ b/spec/support/shared_examples/observability/csp_shared_examples.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+# Verifies that the proper CSP rules for Observabilty UI are applied to a given controller/path
+#
+# The path under test needs to be declared with `let(:tested_path) { .. }` in the context including this example
+#
+# ```
+# it_behaves_like "observability csp policy" do
+# let(:tested_path) { ....the path under test }
+# end
+# ```
+#
+# It optionally supports specifying the controller class handling the tested path as a parameter, e.g.
+#
+# ```
+# it_behaves_like "observability csp policy", Groups::ObservabilityController
+# ```
+# (If not specified it will default to `described_class`)
+#
+RSpec.shared_examples 'observability csp policy' do |controller_class = described_class|
+ include ContentSecurityPolicyHelpers
+
+ let(:observability_url) { Gitlab::Observability.observability_url }
+ let(:signin_url) do
+ Gitlab::Utils.append_path(Gitlab.config.gitlab.url,
+ '/users/sign_in')
+ end
+
+ let(:oauth_url) do
+ Gitlab::Utils.append_path(Gitlab.config.gitlab.url,
+ '/oauth/authorize')
+ end
+
+ before do
+ setup_csp_for_controller(controller_class, csp, any_time: true)
+ end
+
+ subject do
+ get tested_path
+ response.headers['Content-Security-Policy']
+ end
+
+ context 'when there is no CSP config' do
+ let(:csp) { ActionDispatch::ContentSecurityPolicy.new }
+
+ it 'does not add any csp header' do
+ expect(subject).to be_blank
+ end
+ end
+
+ context 'when frame-src exists in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.frame_src 'https://something.test'
+ end
+ end
+
+ it 'appends the proper url to frame-src CSP directives' do
+ expect(subject).to include(
+ "frame-src https://something.test #{observability_url} #{signin_url} #{oauth_url}")
+ end
+ end
+
+ context 'when signin is already present in the policy' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.frame_src signin_url
+ end
+ end
+
+ it 'does not append signin again' do
+ expect(subject).to include(
+ "frame-src #{signin_url} #{observability_url} #{oauth_url};")
+ end
+ end
+
+ context 'when oauth is already present in the policy' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.frame_src oauth_url
+ end
+ end
+
+ it 'does not append oauth again' do
+ expect(subject).to include(
+ "frame-src #{oauth_url} #{observability_url} #{signin_url};")
+ end
+ end
+
+ context 'when default-src exists in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src 'https://something.test'
+ end
+ end
+
+ it 'does not change default-src' do
+ expect(subject).to include(
+ "default-src https://something.test;")
+ end
+
+ it 'appends the proper url to frame-src CSP directives' do
+ expect(subject).to include(
+ "frame-src https://something.test #{observability_url} #{signin_url} #{oauth_url}")
+ end
+ end
+
+ context 'when frame-src and default-src exist in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src 'https://something_default.test'
+ p.frame_src 'https://something.test'
+ end
+ end
+
+ it 'appends to frame-src CSP directives' do
+ expect(subject).to include(
+ "frame-src https://something.test #{observability_url} #{signin_url} #{oauth_url}")
+ expect(subject).to include(
+ "default-src https://something_default.test")
+ end
+ end
+end
diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
index cfcc3615e13..15d56c402d1 100644
--- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb
+++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
@@ -2,13 +2,13 @@
RSpec.shared_examples 'archived project policies' do
let(:feature_write_abilities) do
- described_class.readonly_features.flat_map do |feature|
+ described_class.archived_features.flat_map do |feature|
described_class.create_update_admin_destroy(feature)
end + additional_maintainer_permissions
end
let(:other_write_abilities) do
- described_class.readonly_abilities
+ described_class.archived_abilities
end
context 'when the project is archived' do
diff --git a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
index f7731af8dc6..f70621673d5 100644
--- a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
+++ b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'when regex matching everything is specified' do
- |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ |delete_expectations:, service_response_extra: {}, supports_caching: false|
let(:params) do
{ 'name_regex_delete' => '.*' }
end
@@ -23,6 +23,19 @@ RSpec.shared_examples 'when regex matching everything is specified' do
end
end
+RSpec.shared_examples 'when regex matching everything is specified and latest is not kept' do
+ |delete_expectations:, service_response_extra: {}, supports_caching: false|
+
+ let(:params) do
+ { 'name_regex_delete' => '.*', 'keep_latest' => false }
+ end
+
+ it_behaves_like 'removing the expected tags',
+ service_response_extra: service_response_extra,
+ supports_caching: supports_caching,
+ delete_expectations: delete_expectations
+end
+
RSpec.shared_examples 'when delete regex matching specific tags is used' do
|service_response_extra: {}, supports_caching: false|
let(:params) do
@@ -65,7 +78,7 @@ RSpec.shared_examples 'when delete regex matching specific tags is used with ove
end
RSpec.shared_examples 'with allow regex value' do
- |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ |delete_expectations:, service_response_extra: {}, supports_caching: false|
let(:params) do
{
'name_regex_delete' => '.*',
@@ -80,7 +93,7 @@ RSpec.shared_examples 'with allow regex value' do
end
RSpec.shared_examples 'when keeping only N tags' do
- |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ |delete_expectations:, service_response_extra: {}, supports_caching: false|
let(:params) do
{
'name_regex' => 'A|B.*|C',
@@ -99,7 +112,7 @@ RSpec.shared_examples 'when keeping only N tags' do
end
RSpec.shared_examples 'when not keeping N tags' do
- |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ |delete_expectations:, service_response_extra: {}, supports_caching: false|
let(:params) do
{ 'name_regex' => 'A|B.*|C' }
end
@@ -115,7 +128,7 @@ RSpec.shared_examples 'when not keeping N tags' do
end
RSpec.shared_examples 'when removing keeping only 3' do
- |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ |delete_expectations:, service_response_extra: {}, supports_caching: false|
let(:params) do
{ 'name_regex_delete' => '.*',
'keep_n' => 3 }
@@ -128,7 +141,7 @@ RSpec.shared_examples 'when removing keeping only 3' do
end
RSpec.shared_examples 'when removing older than 1 day' do
- |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ |delete_expectations:, service_response_extra: {}, supports_caching: false|
let(:params) do
{
'name_regex_delete' => '.*',
@@ -143,7 +156,7 @@ RSpec.shared_examples 'when removing older than 1 day' do
end
RSpec.shared_examples 'when combining all parameters' do
- |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ |delete_expectations:, service_response_extra: {}, supports_caching: false|
let(:params) do
{
'name_regex_delete' => '.*',
@@ -159,7 +172,7 @@ RSpec.shared_examples 'when combining all parameters' do
end
RSpec.shared_examples 'when running a container_expiration_policy' do
- |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ |delete_expectations:, service_response_extra: {}, supports_caching: false|
let(:user) { nil }
context 'with valid container_expiration_policy param' do
@@ -191,7 +204,7 @@ RSpec.shared_examples 'not removing anything' do |service_response_extra: {}, su
end
RSpec.shared_examples 'removing the expected tags' do
- |service_response_extra: {}, supports_caching: false, delete_expectations:|
+ |delete_expectations:, service_response_extra: {}, supports_caching: false|
it 'removes the expected tags' do
delete_expectations.each { |expectation| expect_delete(expectation) }
expect_no_caching unless supports_caching
diff --git a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb
index 4d142199c95..1cd529aa50b 100644
--- a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb
@@ -1,21 +1,23 @@
# frozen_string_literal: true
RSpec.shared_examples 'issuable quick actions' do
- QuickAction = Struct.new(:action_text, :expectation, :before_action, keyword_init: true) do
- # Pass a block as :before_action if
- # issuable state needs to be changed before
- # the quick action is executed.
- def call_before_action
- before_action.call if before_action
- end
+ before do
+ stub_const('QuickAction', Struct.new(:action_text, :expectation, :before_action, keyword_init: true) do
+ # Pass a block as :before_action if
+ # issuable state needs to be changed before
+ # the quick action is executed.
+ def call_before_action
+ before_action.call if before_action
+ end
- def skip_access_check
- action_text["/todo"] ||
- action_text["/done"] ||
- action_text["/subscribe"] ||
- action_text["/shrug"] ||
- action_text["/tableflip"]
- end
+ def skip_access_check
+ action_text["/todo"] ||
+ action_text["/done"] ||
+ action_text["/subscribe"] ||
+ action_text["/shrug"] ||
+ action_text["/tableflip"]
+ end
+ end)
end
let(:unlabel_expectation) do
@@ -145,6 +147,12 @@ RSpec.shared_examples 'issuable quick actions' do
}
),
QuickAction.new(
+ action_text: "/labels ~feature",
+ expectation: ->(noteable, can_use_quick_action) {
+ expect(noteable.labels&.last&.id == feature_label.id).to eq(can_use_quick_action)
+ }
+ ),
+ QuickAction.new(
action_text: "/unlabel",
expectation: unlabel_expectation
),
diff --git a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
index 18304951e41..56a1cee44c8 100644
--- a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
@@ -22,6 +22,12 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
end
end
+ def open_create_timelog_form
+ page.within time_tracker_selector do
+ find('[data-testid="add-time-entry-button"]').click
+ end
+ end
+
it 'renders the sidebar component empty state' do
page.within '[data-testid="noTrackingPane"]' do
expect(page).to have_content 'No estimate or time spent'
@@ -74,11 +80,13 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
end
end
- it 'shows the help state when icon is clicked' do
- page.within time_tracker_selector do
- find('[data-testid="helpButton"]').click
- expect(page).to have_content 'Track time with quick actions'
- expect(page).to have_content 'Learn more'
+ it 'shows the create timelog form when add button is clicked' do
+ open_create_timelog_form
+
+ page.within '[data-testid="create-timelog-modal"]' do
+ expect(page).to have_content 'Add time entry'
+ expect(page).to have_content 'Time spent'
+ expect(page).to have_content 'Spent at'
end
end
@@ -123,24 +131,6 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
expect(page).to have_content '1d'
end
end
-
- it 'hides the help state when close icon is clicked' do
- page.within time_tracker_selector do
- find('[data-testid="helpButton"]').click
- find('[data-testid="closeHelpButton"]').click
-
- expect(page).not_to have_content 'Track time with quick actions'
- expect(page).not_to have_content 'Learn more'
- end
- end
-
- it 'displays the correct help url' do
- page.within time_tracker_selector do
- find('[data-testid="helpButton"]').click
-
- expect(find_link('Learn more')[:href]).to have_content('/help/user/project/time_tracking.md')
- end
- end
end
def submit_time(quick_action)
diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
index 629d93676eb..c76bc7c3107 100644
--- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
@@ -593,7 +593,7 @@ RSpec.shared_examples 'delete package endpoint' do
project.add_maintainer(user)
end
- it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'delete_package'
+ it_behaves_like 'a package tracking event', 'API::ConanPackages', 'delete_package'
it 'deletes a package' do
expect { subject }.to change { Packages::Package.count }.from(2).to(1)
@@ -708,7 +708,7 @@ RSpec.shared_examples 'package file download endpoint' do
context 'tracking the conan_package.tgz download' do
let(:package_file) { package.package_files.find_by(file_name: ::Packages::Conan::FileMetadatum::PACKAGE_BINARY) }
- it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'pull_package'
+ it_behaves_like 'a package tracking event', 'API::ConanPackages', 'pull_package'
end
end
@@ -781,7 +781,7 @@ RSpec.shared_examples 'workhorse package file upload endpoint' do
context 'tracking the conan_package.tgz upload' do
let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY }
- it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'push_package'
+ it_behaves_like 'a package tracking event', 'API::ConanPackages', 'push_package'
end
end
@@ -849,12 +849,6 @@ RSpec.shared_examples 'uploads a package file' do
expect(package_file.file_name).to eq(params[:file].original_filename)
end
- it "doesn't attempt to migrate file to object storage" do
- expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
-
- subject
- end
-
context 'with existing package' do
let!(:existing_package) { create(:conan_package, name: 'foo', version: 'bar', project: project) }
@@ -936,8 +930,6 @@ RSpec.shared_examples 'uploads a package file' do
end
end
end
-
- it_behaves_like 'background upload schedules a file migration'
end
end
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 5469fd80a4f..d4479e462af 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
@@ -1,6 +1,15 @@
# frozen_string_literal: true
RSpec.shared_examples 'graphql issue list request spec' do
+ let(:issue_ids) { graphql_dig_at(issues_data, :id) }
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ #{all_graphql_fields_for('issues'.classify)}
+ }
+ QUERY
+ end
+
it_behaves_like 'a working graphql query' do
before do
post_query
@@ -109,10 +118,57 @@ RSpec.shared_examples 'graphql issue list request spec' do
let(:ids) { issue_ids }
end
end
+
+ context 'when filtering by confidentiality' do
+ context 'when fetching confidential issues' do
+ let(:issue_filter_params) { { confidential: true } }
+
+ it 'returns only confidential issues' do
+ post_query
+
+ expect(issue_ids).to match_array(to_gid_list(confidential_issues))
+ end
+
+ context 'when user cannot see confidential issues' do
+ it 'returns an empty list' do
+ post_query(external_user)
+
+ expect(issue_ids).to be_empty
+ end
+ end
+ end
+
+ context 'when fetching non-confidential issues' do
+ let(:issue_filter_params) { { confidential: false } }
+
+ it 'returns only non-confidential issues' do
+ post_query
+
+ expect(issue_ids).to match_array(to_gid_list(non_confidential_issues))
+ end
+
+ context 'when user cannot see confidential issues' do
+ it 'returns an empty list' do
+ post_query(external_user)
+
+ expect(issue_ids).to match_array(to_gid_list(public_non_confidential_issues))
+ end
+ end
+ end
+ end
end
describe 'sorting and pagination' do
context 'when sorting by severity' do
+ let(:expected_severity_sorted_asc) { [issue_c, issue_a, issue_b, issue_e, issue_d] }
+
+ before_all do
+ create(:issuable_severity, issue: issue_a, severity: :unknown)
+ create(:issuable_severity, issue: issue_b, severity: :low)
+ create(:issuable_severity, issue: issue_d, severity: :critical)
+ create(:issuable_severity, issue: issue_e, severity: :high)
+ end
+
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :SEVERITY_ASC }
@@ -147,6 +203,459 @@ RSpec.shared_examples 'graphql issue list request spec' do
end
end
end
+
+ context 'when sorting by due date' do
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :DUE_DATE_ASC }
+ let(:first_param) { 2 }
+ let(:all_records) { to_gid_list(expected_due_date_sorted_asc) }
+ end
+ end
+
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :DUE_DATE_DESC }
+ let(:first_param) { 2 }
+ let(:all_records) { to_gid_list(expected_due_date_sorted_desc) }
+ end
+ end
+ end
+
+ context 'when sorting by relative position' do
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query', is_reversible: true do
+ let(:sort_param) { :RELATIVE_POSITION_ASC }
+ let(:first_param) { 2 }
+ let(:all_records) { to_gid_list(expected_relative_position_sorted_asc) }
+ end
+ end
+ end
+
+ context 'when sorting by label priority' do
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :LABEL_PRIORITY_ASC }
+ let(:first_param) { 2 }
+ let(:all_records) { to_gid_list(expected_label_priority_sorted_asc) }
+ end
+ end
+
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :LABEL_PRIORITY_DESC }
+ let(:first_param) { 2 }
+ let(:all_records) { to_gid_list(expected_label_priority_sorted_desc) }
+ end
+ end
+ end
+
+ context 'when sorting by milestone due date' do
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :MILESTONE_DUE_ASC }
+ let(:first_param) { 2 }
+ let(:all_records) { to_gid_list(expected_milestone_sorted_asc) }
+ end
+ end
+
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :MILESTONE_DUE_DESC }
+ let(:first_param) { 2 }
+ let(:all_records) { to_gid_list(expected_milestone_sorted_desc) }
+ end
+ end
+ end
+ end
+
+ describe 'N+1 query checks' do
+ let(:extra_iid_for_second_query) { issue_b.iid.to_s }
+ let(:search_params) { { iids: [issue_a.iid.to_s] } }
+ let(:issue_filter_params) { search_params }
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ id
+ #{requested_fields}
+ }
+ QUERY
+ end
+
+ def execute_query
+ post_query
+ end
+
+ context 'when requesting `user_notes_count` and `user_discussions_count`' do
+ let(:requested_fields) { 'userNotesCount userDiscussionsCount' }
+
+ before do
+ create_list(:note_on_issue, 2, noteable: issue_a, project: issue_a.project)
+ create(:note_on_issue, noteable: issue_b, project: issue_b.project)
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when requesting `merge_requests_count`' do
+ let(:requested_fields) { 'mergeRequestsCount' }
+
+ before do
+ create_list(:merge_requests_closing_issues, 2, issue: issue_a)
+ create_list(:merge_requests_closing_issues, 3, issue: issue_b)
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when requesting `timelogs`' do
+ let(:requested_fields) { 'timelogs { nodes { timeSpent } }' }
+
+ before do
+ create_list(:issue_timelog, 2, issue: issue_a)
+ create(:issue_timelog, issue: issue_b)
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when requesting `closed_as_duplicate_of`' do
+ let(:requested_fields) { 'closedAsDuplicateOf { id }' }
+ let(:issue_a_dup) { create(:issue, project: issue_a.project) }
+ let(:issue_b_dup) { create(:issue, project: issue_b.project) }
+
+ before do
+ issue_a.update!(duplicated_to_id: issue_a_dup)
+ issue_b.update!(duplicated_to_id: issue_a_dup)
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when award emoji votes' do
+ let(:requested_fields) { 'upvotes downvotes' }
+
+ before do
+ create_list(:award_emoji, 2, name: 'thumbsup', awardable: issue_a)
+ create_list(:award_emoji, 2, name: 'thumbsdown', awardable: issue_b)
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when requesting participants' do
+ let(:search_params) { { iids: [issue_a.iid.to_s, issue_c.iid.to_s] } }
+ let(:requested_fields) { 'participants { nodes { name } }' }
+
+ before do
+ create(:award_emoji, :upvote, awardable: issue_a)
+ create(:award_emoji, :upvote, awardable: issue_b)
+ create(:award_emoji, :upvote, awardable: issue_c)
+
+ note_with_emoji_a = create(:note_on_issue, noteable: issue_a, project: issue_a.project)
+ note_with_emoji_b = create(:note_on_issue, noteable: issue_b, project: issue_b.project)
+ note_with_emoji_c = create(:note_on_issue, noteable: issue_c, project: issue_c.project)
+
+ create(:award_emoji, :upvote, awardable: note_with_emoji_a)
+ create(:award_emoji, :upvote, awardable: note_with_emoji_b)
+ create(:award_emoji, :upvote, awardable: note_with_emoji_c)
+ end
+
+ # Executes 3 extra queries to fetch participant_attrs
+ include_examples 'N+1 query check', threshold: 3
+ end
+
+ context 'when requesting labels', :use_sql_query_cache do
+ let(:requested_fields) { 'labels { nodes { id } }' }
+ let(:extra_iid_for_second_query) { same_project_issue2.iid.to_s }
+ let(:search_params) { { iids: [same_project_issue1.iid.to_s] } }
+
+ before do
+ current_project = same_project_issue1.project
+ project_labels = create_list(:label, 2, project: current_project)
+ group_labels = create_list(:group_label, 2, group: current_project.group)
+
+ same_project_issue1.update!(labels: [project_labels.first, group_labels.first].flatten)
+ same_project_issue2.update!(labels: [project_labels, group_labels].flatten)
+ end
+
+ include_examples 'N+1 query check', skip_cached: false
+ end
+ end
+
+ context 'when confidential issues exist' do
+ context 'when user can see confidential issues' do
+ it 'includes confidential issues' do
+ post_query
+
+ all_issues = confidential_issues + non_confidential_issues
+
+ expect(issue_ids).to match_array(to_gid_list(all_issues))
+ expect(issues_data.pluck('confidential')).to match_array(all_issues.map(&:confidential))
+ end
+ end
+
+ context 'when user cannot see confidential issues' do
+ let(:current_user) { external_user }
+
+ it 'does not include confidential issues' do
+ post_query
+
+ expect(issue_ids).to match_array(to_gid_list(public_non_confidential_issues))
+ end
+ end
+ end
+
+ context 'when limiting the number of results' do
+ let(:issue_limit) { 1 }
+ let(:issue_filter_params) { { first: issue_limit } }
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_query
+ end
+
+ it 'only returns N issues' do
+ expect(issues_data.size).to eq(issue_limit)
+ end
+ end
+
+ context 'when no limit is provided' do
+ let(:issue_limit) { nil }
+
+ it 'returns all issues' do
+ post_query
+
+ expect(issues_data.size).to be > 1
+ end
+ end
+
+ it 'is expected to check permissions on the first issue only' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ # Newest first, we only want to see the newest checked
+ expect(Ability).not_to receive(:allowed?).with(current_user, :read_issue, issues.first)
+
+ post_query
+ end
+ end
+
+ context 'when the user does not have access to the issue' do
+ let(:current_user) { external_user }
+
+ it 'returns no issues' do
+ public_projects.each do |public_project|
+ public_project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+ end
+
+ post_query
+
+ expect(issues_data).to eq([])
+ end
+ end
+
+ context 'when fetching escalation status' do
+ let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) }
+
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ id
+ escalationStatus
+ }
+ QUERY
+ end
+
+ before do
+ issue_a.update_columns(issue_type: Issue.issue_types[:incident])
+ end
+
+ it 'returns the escalation status values' do
+ post_query
+
+ statuses = issues_data.pluck('escalationStatus')
+
+ expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil, nil, nil, nil)
+ end
+
+ it 'avoids N+1 queries', :aggregate_failures do
+ control = ActiveRecord::QueryRecorder.new { run_with_clean_state(query, context: { current_user: current_user }) }
+
+ new_incident = create(:incident, project: public_projects.first)
+ create(:incident_management_issuable_escalation_status, issue: new_incident)
+
+ expect { run_with_clean_state(query, context: { current_user: current_user }) }.not_to exceed_query_limit(control)
+ end
+ end
+
+ context 'when fetching alert management alert' do
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ iid
+ alertManagementAlert {
+ title
+ }
+ alertManagementAlerts {
+ nodes {
+ title
+ }
+ }
+ }
+ QUERY
+ end
+
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new { post_query }
+
+ create(:alert_management_alert, :with_incident, project: public_projects.first)
+
+ expect { post_query }.not_to exceed_query_limit(control)
+ end
+
+ it 'returns the alert data' do
+ post_query
+
+ alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlert', 'title') }
+ expected_titles = issues.map { |issue| issue.alert_management_alerts.first&.title }
+
+ expect(alert_titles).to contain_exactly(*expected_titles)
+ end
+
+ it 'returns the alerts data' do
+ post_query
+
+ alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlerts', 'nodes') }
+ expected_titles = issues.map do |issue|
+ issue.alert_management_alerts.map { |alert| { 'title' => alert.title } }
+ end
+
+ expect(alert_titles).to contain_exactly(*expected_titles)
+ end
+ end
+
+ context 'when fetching customer_relations_contacts' do
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ id
+ customerRelationsContacts {
+ nodes {
+ firstName
+ }
+ }
+ }
+ QUERY
+ end
+
+ def clean_state_query
+ run_with_clean_state(query, context: { current_user: current_user })
+ end
+
+ it 'avoids N+1 queries' do
+ create(:issue_customer_relations_contact, :for_issue, issue: issue_a)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) { clean_state_query }
+
+ create(:issue_customer_relations_contact, :for_issue, issue: issue_a)
+
+ expect { clean_state_query }.not_to exceed_all_query_limit(control)
+ end
+ end
+
+ context 'when fetching labels' do
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ id
+ labels {
+ nodes {
+ id
+ }
+ }
+ }
+ QUERY
+ end
+
+ before do
+ issues.each do |issue|
+ # create a label for each issue we have to properly test N+1
+ label = create(:label, project: issue.project)
+ issue.update!(labels: [label])
+ end
+ end
+
+ def response_label_ids(response_data)
+ response_data.map do |node|
+ node['labels']['nodes'].pluck('id')
+ end.flatten
+ end
+
+ def labels_as_global_ids(issues)
+ issues.map(&:labels).flatten.map(&:to_global_id).map(&:to_s)
+ end
+
+ it 'avoids N+1 queries', :aggregate_failures do
+ control = ActiveRecord::QueryRecorder.new { post_query }
+ expect(issues_data.count).to eq(5)
+ expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(issues))
+
+ public_project = public_projects.first
+ new_issues = issues + [
+ create(:issue, project: public_project, labels: [create(:label, project: public_project)])
+ ]
+
+ expect { post_query }.not_to exceed_query_limit(control)
+
+ expect(issues_data.count).to eq(6)
+ expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(new_issues))
+ end
+ end
+
+ context 'when fetching assignees' do
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ id
+ assignees {
+ nodes {
+ id
+ }
+ }
+ }
+ QUERY
+ end
+
+ before do
+ issues.each do |issue|
+ # create an assignee for each issue we have to properly test N+1
+ assignee = create(:user)
+ issue.update!(assignees: [assignee])
+ end
+ end
+
+ def response_assignee_ids(response_data)
+ response_data.map do |node|
+ node['assignees']['nodes'].pluck('id')
+ end.flatten
+ end
+
+ def assignees_as_global_ids(issues)
+ issues.map(&:assignees).flatten.map(&:to_global_id).map(&:to_s)
+ end
+
+ it 'avoids N+1 queries', :aggregate_failures do
+ control = ActiveRecord::QueryRecorder.new { post_query }
+ expect(issues_data.count).to eq(5)
+ expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(issues))
+
+ public_project = public_projects.first
+ new_issues = issues + [create(:issue, project: public_project, assignees: [create(:user)])]
+
+ expect { post_query }.not_to exceed_query_limit(control)
+
+ expect(issues_data.count).to eq(6)
+ expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(new_issues))
+ end
end
it 'includes a web_url' do
@@ -167,4 +676,8 @@ RSpec.shared_examples 'graphql issue list request spec' do
def to_gid_list(instance_list)
instance_list.map { |instance| instance.to_gid.to_s }
end
+
+ def issues_data
+ graphql_data.dig(*issue_nodes_path)
+ end
end
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
index fb4aacfd7a9..f5835460a77 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
@@ -62,6 +62,21 @@ RSpec.shared_examples 'group and project packages query' do
it 'returns the count of the packages' do
expect(packages_count).to eq(4)
end
+
+ context '_links' do
+ let_it_be(:errored_package) { create(:maven_package, :error, project: project1) }
+
+ let(:package_web_paths) { graphql_data_at(resource_type, :packages, :nodes, :_links, :web_path) }
+
+ it 'does not contain the web path of errored package' do
+ expect(package_web_paths.compact).to contain_exactly(
+ "/#{project1.full_path}/-/packages/#{npm_package.id}",
+ "/#{project1.full_path}/-/packages/#{maven_package.id}",
+ "/#{project2.full_path}/-/packages/#{debian_package.id}",
+ "/#{project2.full_path}/-/packages/#{composer_package.id}"
+ )
+ end
+ end
end
context 'when the user does not have access to the resource' do
@@ -139,7 +154,7 @@ RSpec.shared_examples 'group and project packages query' do
end
it 'throws an error' do
- expect_graphql_errors_to_include(/Argument \'sort\' on Field \'packages\' has an invalid value/)
+ expect_graphql_errors_to_include(/Argument 'sort' on Field 'packages' has an invalid value/)
end
end
diff --git a/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb
index 54cc13fac94..6b4d8cae2ce 100644
--- a/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
-RSpec.shared_examples 'perform graphql requests for AccessLevel type objects' do |access_level_kind|
+RSpec.shared_examples 'a GraphQL query for access levels' do |access_level_kind|
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
let_it_be(:variables) { { path: project.full_path } }
- let(:fields) { all_graphql_fields_for("#{access_level_kind.to_s.classify}AccessLevel", max_depth: 2) }
+ let(:fields) { all_graphql_fields_for("#{access_level_kind.to_s.classify}AccessLevel") }
let(:access_levels) { protected_branch.public_send("#{access_level_kind}_access_levels") }
let(:access_levels_count) { access_levels.size }
let(:maintainer_access_level) { access_levels.for_role.first }
@@ -61,17 +61,35 @@ RSpec.shared_examples 'perform graphql requests for AccessLevel type objects' do
create(:protected_branch, "maintainers_can_#{access_level_kind}", project: project)
end
- before do
- post_graphql(query, current_user: current_user, variables: variables)
+ describe 'query' do
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+ expect_graphql_errors_to_be_empty
+
+ create("protected_branch_#{access_level_kind}_access_level", protected_branch: protected_branch)
+
+ expect do
+ post_graphql(query, current_user: current_user, variables: variables)
+ end.not_to exceed_all_query_limit(control)
+ expect_graphql_errors_to_be_empty
+ end
end
- it_behaves_like 'a working graphql query'
+ describe 'response' do
+ before do
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
- it 'returns all the access level attributes' do
- expect(maintainer_access_level_data['accessLevel']).to eq(maintainer_access_level.access_level)
- expect(maintainer_access_level_data['accessLevelDescription']).to eq(maintainer_access_level.humanize)
- expect(maintainer_access_level_data.dig('group', 'name')).to be_nil
- expect(maintainer_access_level_data.dig('user', 'name')).to be_nil
+ it 'returns all the access level attributes' do
+ expect(maintainer_access_level_data['accessLevel']).to eq(maintainer_access_level.access_level)
+ expect(maintainer_access_level_data['accessLevelDescription']).to eq(maintainer_access_level.humanize)
+ expect(maintainer_access_level_data.dig('group', 'name')).to be_nil
+ expect(maintainer_access_level_data.dig('user', 'name')).to be_nil
+ end
end
end
end
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 8bf6b162508..7803f0ff04d 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
@@ -156,25 +156,13 @@ RSpec.shared_examples 'process helm upload' do |user_type, status|
end
context 'and direct upload disabled' do
- context 'and background upload disabled' do
- let(:fog_connection) do
- stub_package_file_object_storage(direct_upload: false, background_upload: false)
- end
-
- it_behaves_like 'creates helm package files'
+ let(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: false)
end
- context 'and background upload enabled' do
- let(:fog_connection) do
- stub_package_file_object_storage(direct_upload: false, background_upload: true)
- end
-
- it_behaves_like 'creates helm package files'
- end
+ it_behaves_like 'creates helm package files'
end
end
-
- it_behaves_like 'background upload schedules a file migration'
end
end
diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
index 11f9565989f..efe5ed3bcf9 100644
--- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
@@ -159,7 +159,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
expect do
post api(uri, user), params: { body: 'hi!' }
- end.to change(Event, :count).by(1)
+ end.to change { Event.count }.by(1)
end
context 'setting created_at' do
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index 85ac2b5e1ea..b55639a6b82 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -323,6 +323,171 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
end
end
+RSpec.shared_examples 'handling audit request' do |path:, scope: :project|
+ using RSpec::Parameterized::TableSyntax
+
+ let(:headers) { {} }
+ let(:params) do
+ ActiveSupport::Gzip.compress(
+ Gitlab::Json.dump({
+ '@gitlab-org/npm-test': ['1.0.6'],
+ 'call-bind': ['1.0.2']
+ })
+ )
+ end
+
+ let(:default_headers) do
+ { 'HTTP_CONTENT_ENCODING' => 'gzip', 'CONTENT_TYPE' => 'application/json' }
+ end
+
+ subject { post(url, headers: headers.merge(default_headers), params: params) }
+
+ shared_examples 'accept audit request' do |status:|
+ it 'accepts the audit request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+ expect(response.media_type).to eq('application/json')
+ expect(json_response).to eq([])
+ end
+ end
+
+ shared_examples 'reject audit request' do |status:|
+ it 'rejects the audit request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+ end
+ end
+
+ shared_examples 'redirect audit request' do |status:|
+ it 'redirects audit request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+ expect(response.headers['Location']).to eq("https://registry.npmjs.org/-/npm/v1/security/#{path}")
+ end
+ end
+
+ shared_examples 'handling all conditions' do
+ include_context 'dependency proxy helpers context'
+
+ where(:auth, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do
+ nil | true | :public | nil | :reject | :unauthorized
+ nil | false | :public | nil | :reject | :unauthorized
+ nil | true | :private | nil | :reject | :unauthorized
+ nil | false | :private | nil | :reject | :unauthorized
+ nil | true | :internal | nil | :reject | :unauthorized
+ nil | false | :internal | nil | :reject | :unauthorized
+
+ :oauth | true | :public | :guest | :redirect | :temporary_redirect
+ :oauth | true | :public | :reporter | :redirect | :temporary_redirect
+ :oauth | false | :public | :guest | :accept | :ok
+ :oauth | false | :public | :reporter | :accept | :ok
+ :oauth | true | :private | :reporter | :redirect | :temporary_redirect
+ :oauth | false | :private | :guest | :reject | :forbidden
+ :oauth | false | :private | :reporter | :accept | :ok
+ :oauth | true | :private | :guest | :redirect | :temporary_redirect
+ :oauth | true | :internal | :guest | :redirect | :temporary_redirect
+ :oauth | true | :internal | :reporter | :redirect | :temporary_redirect
+ :oauth | false | :internal | :guest | :accept | :ok
+ :oauth | false | :internal | :reporter | :accept | :ok
+
+ :personal_access_token | true | :public | :guest | :redirect | :temporary_redirect
+ :personal_access_token | true | :public | :reporter | :redirect | :temporary_redirect
+ :personal_access_token | false | :public | :guest | :accept | :ok
+ :personal_access_token | false | :public | :reporter | :accept | :ok
+ :personal_access_token | true | :private | :guest | :redirect | :temporary_redirect
+ :personal_access_token | true | :private | :reporter | :redirect | :temporary_redirect
+ :personal_access_token | false | :private | :guest | :reject | :forbidden # instance might fail
+ :personal_access_token | false | :private | :reporter | :accept | :ok
+ :personal_access_token | true | :internal | :guest | :redirect | :temporary_redirect
+ :personal_access_token | true | :internal | :reporter | :redirect | :temporary_redirect
+ :personal_access_token | false | :internal | :guest | :accept | :ok
+ :personal_access_token | false | :internal | :reporter | :accept | :ok
+
+ :job_token | true | :public | :developer | :redirect | :temporary_redirect
+ :job_token | false | :public | :developer | :accept | :ok
+ :job_token | true | :private | :developer | :redirect | :temporary_redirect
+ :job_token | false | :private | :developer | :accept | :ok
+ :job_token | true | :internal | :developer | :redirect | :temporary_redirect
+ :job_token | false | :internal | :developer | :accept | :ok
+
+ :deploy_token | true | :public | nil | :redirect | :temporary_redirect
+ :deploy_token | false | :public | nil | :accept | :ok
+ :deploy_token | true | :private | nil | :redirect | :temporary_redirect
+ :deploy_token | false | :private | nil | :accept | :ok
+ :deploy_token | true | :internal | nil | :redirect | :temporary_redirect
+ :deploy_token | false | :internal | nil | :accept | :ok
+ end
+
+ with_them do
+ let(:headers) do
+ case auth
+ when :oauth
+ build_token_auth_header(token.plaintext_token)
+ when :personal_access_token
+ build_token_auth_header(personal_access_token.token)
+ when :job_token
+ build_token_auth_header(job.token)
+ when :deploy_token
+ build_token_auth_header(deploy_token.token)
+ else
+ {}
+ end
+ end
+
+ before do
+ project.send("add_#{user_role}", user) if user_role
+ project.update!(visibility: visibility.to_s)
+
+ if scope == :instance
+ allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
+ else
+ allow_fetch_cascade_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
+ end
+ end
+
+ example_name = "#{params[:expected_result]} audit request"
+ status = params[:expected_status]
+
+ if scope == :instance && params[:expected_status] != :unauthorized
+ if params[:request_forward]
+ example_name = 'redirect audit request'
+ status = :temporary_redirect
+ else
+ example_name = 'reject audit request'
+ status = :not_found
+ end
+ end
+
+ it_behaves_like example_name, status: status
+ end
+ end
+
+ context 'with a group namespace' do
+ it_behaves_like 'handling all conditions'
+ end
+
+ context 'with a developer' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'with a job token' do
+ let(:headers) { build_token_auth_header(job.token) }
+
+ before do
+ job.update!(status: :success)
+ end
+
+ it_behaves_like 'reject audit request', status: :unauthorized
+ end
+ end
+end
+
RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
using RSpec::Parameterized::TableSyntax
include_context 'set package name from package name type'
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 fdd55893deb..bace570e47a 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
@@ -224,25 +224,13 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member =
end
context 'and direct upload disabled' do
- context 'and background upload disabled' do
- let(:fog_connection) do
- stub_package_file_object_storage(direct_upload: false, background_upload: false)
- end
-
- it_behaves_like 'creates nuget package files'
+ let(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: false)
end
- context 'and background upload enabled' do
- let(:fog_connection) do
- stub_package_file_object_storage(direct_upload: false, background_upload: true)
- end
-
- it_behaves_like 'creates nuget package files'
- end
+ it_behaves_like 'creates nuget package files'
end
end
-
- it_behaves_like 'background upload schedules a file migration'
end
end
@@ -507,7 +495,7 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_headers) }
- let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } }
+ let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' } }
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
index 860cb1b1d86..98264baa61d 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -142,15 +142,26 @@ RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: fa
end
end
-RSpec.shared_examples 'a package tracking event' do |category, action|
+RSpec.shared_examples 'a package tracking event' do |category, action, service_ping_context = true|
before do
stub_feature_flags(collect_package_events: true)
end
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
+ event: snowplow_gitlab_standard_context[:property]).to_h]
+ end
+
it "creates a gitlab tracking event #{action}", :snowplow, :aggregate_failures do
expect { subject }.to change { Packages::Event.count }.by(1)
- expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context)
+ if service_ping_context
+ expect_snowplow_event(category: category, action: action,
+ label: "redis_hll_counters.user_packages.user_packages_total_unique_counts_monthly",
+ context: context, **snowplow_gitlab_standard_context)
+ else
+ expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context)
+ end
end
end
diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
index a9b44015206..a267476b7cb 100644
--- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
@@ -95,25 +95,13 @@ RSpec.shared_examples 'PyPI package creation' do |user_type, status, add_member
end
context 'and direct upload disabled' do
- context 'and background upload disabled' do
- let(:fog_connection) do
- stub_package_file_object_storage(direct_upload: false, background_upload: false)
- end
-
- it_behaves_like 'creating pypi package files'
+ let(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: false)
end
- context 'and background upload enabled' do
- let(:fog_connection) do
- stub_package_file_object_storage(direct_upload: false, background_upload: true)
- end
-
- it_behaves_like 'creating pypi package files'
- end
+ it_behaves_like 'creating pypi package files'
end
end
-
- it_behaves_like 'background upload schedules a file migration'
end
end
@@ -285,7 +273,7 @@ RSpec.shared_examples 'pypi simple API endpoint' do
let(:url) { "/projects/#{project.id}/packages/pypi/simple/my-package" }
let(:headers) { basic_auth_header(user.username, personal_access_token.token) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: group, property: 'i_package_pypi_user' } }
it_behaves_like 'PyPI package versions', :developer, :success
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 2d036cb2aa3..2154a76d765 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
@@ -71,11 +71,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
get_container_repository_storage_moves
json_ids = json_response.map { |storage_move| storage_move['id'] }
- expect(json_ids).to eq([
- storage_move.id,
- storage_move_middle.id,
- storage_move_oldest.id
- ])
+ expect(json_ids).to eq([storage_move.id, storage_move_middle.id, storage_move_oldest.id])
end
describe 'permissions' do
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 f075927e7bf..da09d70c777 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
@@ -122,21 +122,11 @@ RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_membe
end
context 'and direct upload disabled' do
- context 'and background upload disabled' do
- let(:fog_connection) do
- stub_package_file_object_storage(direct_upload: false, background_upload: false)
- end
-
- it_behaves_like 'creates rubygems package files'
+ let(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: false)
end
- context 'and background upload enabled' do
- let(:fog_connection) do
- stub_package_file_object_storage(direct_upload: false, background_upload: true)
- end
-
- it_behaves_like 'creates rubygems package files'
- end
+ it_behaves_like 'creates rubygems package files'
end
end
end
diff --git a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb
index bdff2c65691..ae2855083f6 100644
--- a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb
@@ -264,21 +264,11 @@ RSpec.shared_examples 'process terraform module upload' do |user_type, status, a
end
context 'and direct upload disabled' do
- context 'and background upload disabled' do
- let(:fog_connection) do
- stub_package_file_object_storage(direct_upload: false, background_upload: false)
- end
-
- it_behaves_like 'creates terraform module package files'
+ let(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: false)
end
- context 'and background upload enabled' do
- let(:fog_connection) do
- stub_package_file_object_storage(direct_upload: false, background_upload: true)
- end
-
- it_behaves_like 'creates terraform module package files'
- end
+ it_behaves_like 'creates terraform module package files'
end
end
end
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index 11759b6671f..82ed6eb4c95 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -68,6 +68,7 @@ RSpec.shared_examples 'rate-limited token requests' do
# Set low limits
settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
+ travel_back
end
after do
@@ -220,6 +221,7 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
# Set low limits
settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
+ travel_back
end
after do
@@ -436,6 +438,7 @@ RSpec.shared_examples 'rate-limited unauthenticated requests' do
# Set low limits
settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
+ travel_back
end
context 'when the throttle is enabled' do
diff --git a/spec/support/shared_examples/security_training_providers_importer.rb b/spec/support/shared_examples/security_training_providers_importer.rb
index 568e3e1a4f2..69d92964270 100644
--- a/spec/support/shared_examples/security_training_providers_importer.rb
+++ b/spec/support/shared_examples/security_training_providers_importer.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples 'security training providers importer' do
end
it 'upserts security training providers' do
- expect { 2.times { subject } }.to change(security_training_providers, :count).from(0).to(2)
+ expect { 2.times { subject } }.to change { security_training_providers.count }.from(0).to(2)
expect(security_training_providers.all.map(&:name)).to match_array(['Kontra', 'Secure Code Warrior'])
end
end
diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb
index 0db9519f760..6a9da91eaa7 100644
--- a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb
@@ -11,7 +11,7 @@ RSpec.shared_examples 'creates an alert management alert or errors' do
it 'creates AlertManagement::Alert' do
expect(Gitlab::AppLogger).not_to receive(:warn)
- expect { subject }.to change(AlertManagement::Alert, :count).by(1)
+ expect { subject }.to change { AlertManagement::Alert.count }.by(1)
end
it 'executes the alert service hooks' do
@@ -118,7 +118,7 @@ end
RSpec.shared_examples 'does not create an alert management alert' do
specify do
- expect { subject }.not_to change(AlertManagement::Alert, :count)
+ expect { subject }.not_to change { AlertManagement::Alert.count }
end
end
diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb
index 1973577d742..2740b6bf59d 100644
--- a/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'closes related incident if enabled' do
specify do
expect { Sidekiq::Testing.inline! { subject } }
.to change { alert.issue.reload.closed? }.from(false).to(true)
- .and change(ResourceStateEvent, :count).by(1)
+ .and change { ResourceStateEvent.count }.by(1)
end
end
diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb
index 57d598c0259..2d0815ba27c 100644
--- a/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb
@@ -19,7 +19,7 @@ RSpec.shared_examples 'creates expected system notes for alert' do |*notes|
end
it "for #{notes.join(', ')}" do
- expect { subject }.to change(Note, :count).by(expected_note_count)
+ expect { subject }.to change { Note.count }.by(expected_note_count)
expected_notes.each_value.with_index do |value, index|
expect(new_notes[index]).to include(value)
@@ -29,6 +29,6 @@ end
RSpec.shared_examples 'does not create a system note for alert' do
specify do
- expect { subject }.not_to change(Note, :count)
+ expect { subject }.not_to change { Note.count }
end
end
diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb
index b46ace1824a..b8fc2eb5475 100644
--- a/spec/support/shared_examples/services/alert_management_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb
@@ -68,8 +68,8 @@ RSpec.shared_examples 'processes one firing and one resolved prometheus alerts'
expect(Gitlab::AppLogger).not_to receive(:warn)
expect { subject }
- .to change(AlertManagement::Alert, :count).by(1)
- .and change(Note, :count).by(1)
+ .to change { AlertManagement::Alert.count }.by(1)
+ .and change { Note.count }.by(1)
expect(subject).to be_success
expect(subject.payload).to eq({})
diff --git a/spec/support/shared_examples/services/approval_state_updated_trigger_shared_examples.rb b/spec/support/shared_examples/services/approval_state_updated_trigger_shared_examples.rb
new file mode 100644
index 00000000000..455fd308be8
--- /dev/null
+++ b/spec/support/shared_examples/services/approval_state_updated_trigger_shared_examples.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'triggers GraphQL subscription mergeRequestApprovalStateUpdated' do
+ specify do
+ expect(GraphqlTriggers).to receive(:merge_request_approval_state_updated).with(merge_request)
+
+ action
+ end
+end
+
+RSpec.shared_examples 'does not trigger GraphQL subscription mergeRequestApprovalStateUpdated' do
+ specify do
+ expect(GraphqlTriggers).not_to receive(:merge_request_approval_state_updated)
+
+ action
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb
index f28c78aec97..67a5af587e9 100644
--- a/spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb
@@ -3,7 +3,7 @@
RSpec.shared_examples 'boards create service' do
context 'when parent does not have a board' do
it 'creates a new board' do
- expect { service.execute }.to change(Board, :count).by(1)
+ expect { service.execute }.to change { Board.count }.by(1)
end
it 'creates the default lists' do
@@ -23,7 +23,7 @@ RSpec.shared_examples 'boards create service' do
it 'does not create a new board' do
expect(service).to receive(:can_create_board?) { false }
- expect { service.execute }.not_to change(parent.boards, :count)
+ expect { service.execute }.not_to change { parent.boards.count }
end
end
end
diff --git a/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb
index fd832d4484d..80d5c771abd 100644
--- a/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'boards list service' do
it 'does not create a new board' do
- expect { service.execute }.not_to change(parent.boards, :count)
+ expect { service.execute }.not_to change { parent.boards.count }
end
it 'returns parent boards' do
diff --git a/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb
index 68ea460dabc..8bf01ad84ff 100644
--- a/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'boards recent visit' do
describe '#visited' do
it 'creates a visit if one does not exists' do
- expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
+ expect { described_class.visited!(user, board) }.to change { described_class.count }.by(1)
end
shared_examples 'was visited previously' do
diff --git a/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb
index af88644ced7..52d427d6684 100644
--- a/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'lists destroy service' do
it 'removes list from board' do
service = described_class.new(parent, user)
- expect { service.execute(list) }.to change(board.lists, :count).by(-1)
+ expect { service.execute(list) }.to change { board.lists.count }.by(-1)
end
it 'decrements position of higher lists' do
@@ -24,6 +24,6 @@ RSpec.shared_examples 'lists destroy service' do
it 'does not remove list from board when list type is closed' do
service = described_class.new(parent, user)
- expect { service.execute(closed_list) }.not_to change(board.lists, :count)
+ expect { service.execute(closed_list) }.not_to change { board.lists.count }
end
end
diff --git a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb
index e1143562661..b9f28fab558 100644
--- a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'lists list service' do
let!(:backlog_list) { create_backlog_list(board) }
it 'does not create a backlog list' do
- expect { service.execute(board) }.not_to change(board.lists, :count)
+ expect { service.execute(board) }.not_to change { board.lists.count }
end
it "returns board's lists" do
@@ -35,11 +35,11 @@ RSpec.shared_examples 'lists list service' do
context 'when the board does not have a backlog list' do
it 'creates a backlog list' do
- expect { service.execute(board) }.to change(board.lists, :count).by(1)
+ expect { service.execute(board) }.to change { board.lists.count }.by(1)
end
it 'does not create a backlog list when create_default_lists is false' do
- expect { service.execute(board, create_default_lists: false) }.not_to change(board.lists, :count)
+ expect { service.execute(board, create_default_lists: false) }.not_to change { board.lists.count }
end
it "returns board's lists" do
diff --git a/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb b/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb
index 28bf46a57d5..38e19e58706 100644
--- a/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb
+++ b/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'updating the container expiration policy attributes' do |mode:, from: {}, to:|
+RSpec.shared_examples 'updating the container expiration policy attributes' do |mode:, to:, from: {}|
if mode == :create
it 'creates a new container expiration policy' do
expect { subject }
diff --git a/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb b/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb
index f6692646ca8..dcc9c3d898f 100644
--- a/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb
+++ b/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'updating the dependency proxy image ttl policy attributes' do |from: {}, to:|
+RSpec.shared_examples 'updating the dependency proxy image ttl policy attributes' do |to:, from: {}|
it_behaves_like 'not creating the dependency proxy image ttl policy'
it 'updates the dependency proxy image ttl policy' do
diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb
index b533b095aac..a87e7c1f801 100644
--- a/spec/support/shared_examples/services/incident_shared_examples.rb
+++ b/spec/support/shared_examples/services/incident_shared_examples.rb
@@ -55,7 +55,7 @@ RSpec.shared_examples 'incident management label service' do
shared_examples 'existing label' do
it 'returns the existing label' do
- expect { execute }.not_to change(Label, :count)
+ expect { execute }.not_to change { Label.count }
expect(execute).to be_success
expect(execute.payload).to eq(label: label)
@@ -64,7 +64,7 @@ RSpec.shared_examples 'incident management label service' do
shared_examples 'new label' do
it 'creates a new label' do
- expect { execute }.to change(Label, :count).by(1)
+ expect { execute }.to change { Label.count }.by(1)
label = project.reload.labels.last
expect(execute).to be_success
diff --git a/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb
index 3d90885dd6f..ff7acc7e907 100644
--- a/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable/update_service_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples_for 'issuable update service updating last_edited_at value
let(:update_params) { { title: 'updated title' } }
it 'does not update last_edited values' do
- expect { update_issuable }.to change(issuable, :title).from(issuable.title).to('updated title').and(
+ expect { update_issuable }.to change { issuable.title }.from(issuable.title).to('updated title').and(
not_change(issuable, :last_edited_at)
).and(
not_change(issuable, :last_edited_by)
@@ -19,10 +19,10 @@ RSpec.shared_examples_for 'issuable update service updating last_edited_at value
it 'updates last_edited values' do
expect do
update_issuable
- end.to change(issuable, :description).from(issuable.description).to('updated description').and(
- change(issuable, :last_edited_at)
+ end.to change { issuable.description }.from(issuable.description).to('updated description').and(
+ change { issuable.last_edited_at }
).and(
- change(issuable, :last_edited_by)
+ change { issuable.last_edited_by }
)
end
end
diff --git a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
index 65351ac94ab..12f2b5d78a5 100644
--- a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
@@ -24,7 +24,7 @@ RSpec.shared_examples 'issuable link creation' do
end
it 'no relationship is created' do
- expect { subject }.not_to change(issuable_link_class, :count)
+ expect { subject }.not_to change { issuable_link_class.count }
end
end
@@ -38,7 +38,7 @@ RSpec.shared_examples 'issuable link creation' do
end
it 'no relationship is created' do
- expect { subject }.not_to change(issuable_link_class, :count)
+ expect { subject }.not_to change { issuable_link_class.count }
end
end
@@ -54,7 +54,7 @@ RSpec.shared_examples 'issuable link creation' do
end
it 'no relationship is created' do
- expect { subject }.not_to change(issuable_link_class, :count)
+ expect { subject }.not_to change { issuable_link_class.count }
end
end
@@ -64,7 +64,7 @@ RSpec.shared_examples 'issuable link creation' do
end
it 'creates relationships' do
- expect { subject }.to change(issuable_link_class, :count).by(2)
+ expect { subject }.to change { issuable_link_class.count }.by(2)
expect(issuable_link_class.find_by!(target: issuable2)).to have_attributes(source: issuable, link_type: 'relates_to')
expect(issuable_link_class.find_by!(target: issuable3)).to have_attributes(source: issuable, link_type: 'relates_to')
diff --git a/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb b/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb
index 5e80014da1d..cc170c6544d 100644
--- a/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples 'a destroyable issuable link' do
end
it 'removes related issue' do
- expect { subject }.to change(issuable_link.class, :count).by(-1)
+ expect { subject }.to change { issuable_link.class.count }.by(-1)
end
it 'creates notes' do
@@ -28,7 +28,7 @@ RSpec.shared_examples 'a destroyable issuable link' do
context 'when failing to remove an issuable link' do
it 'does not remove relation' do
- expect { subject }.not_to change(issuable_link.class, :count).from(1)
+ expect { subject }.not_to change { issuable_link.class.count }.from(1)
end
it 'does not create notes' do
diff --git a/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb b/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb
index f7a6bd3676a..11a786fdefb 100644
--- a/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb
+++ b/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'updating the namespace package setting attributes' do |from: {}, to:|
+RSpec.shared_examples 'updating the namespace package setting attributes' do |to:, from: {}|
it_behaves_like 'not creating the namespace package setting'
it 'updates the namespace package setting' do
diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb
index ca4dea90c55..e0dd08ec50e 100644
--- a/spec/support/shared_examples/services/packages_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -188,20 +188,6 @@ RSpec.shared_examples 'returns paginated packages' do
end
end
-RSpec.shared_examples 'background upload schedules a file migration' do
- context 'background upload enabled' do
- before do
- stub_package_file_object_storage(background_upload: true)
- end
-
- it 'schedules migration of file to object storage' do
- expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('Packages::PackageFileUploader', 'Packages::PackageFile', :file, kind_of(Numeric))
-
- subject
- end
- end
-end
-
RSpec.shared_context 'package filter context' do
def package_filter_url(filter, param)
"/projects/#{project.id}/packages?package_#{filter}=#{param}"
diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
index 14af35e58b7..9f940d27341 100644
--- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
@@ -71,7 +71,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
it 'does not enqueue a GC run' do
expect { subject.execute }
- .not_to change(Projects::GitGarbageCollectWorker.jobs, :count)
+ .not_to change { Projects::GitGarbageCollectWorker.jobs.count }
end
end
@@ -84,12 +84,12 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
stub_application_setting(housekeeping_enabled: false)
expect { subject.execute }
- .not_to change(Projects::GitGarbageCollectWorker.jobs, :count)
+ .not_to change { Projects::GitGarbageCollectWorker.jobs.count }
end
it 'enqueues a GC run' do
expect { subject.execute }
- .to change(Projects::GitGarbageCollectWorker.jobs, :count).by(1)
+ .to change { Projects::GitGarbageCollectWorker.jobs.count }.by(1)
end
end
end
diff --git a/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb
index 4c00faee56b..8a937303711 100644
--- a/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb
+++ b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb
@@ -12,7 +12,7 @@ RSpec.shared_examples 'housekeeps repository' do
expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original
Sidekiq::Testing.fake! do
- expect { subject.execute }.to change(resource.git_garbage_collect_worker_klass.jobs, :size).by(1)
+ expect { subject.execute }.to change { resource.git_garbage_collect_worker_klass.jobs.size }.by(1)
end
end
@@ -71,9 +71,6 @@ RSpec.shared_examples 'housekeeps repository' do
# At push 10, 20, ... (except those above)
expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid)
.exactly(16).times
- # At push 6, 12, 18, ... (except those above)
- expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :pack_refs, :the_lease_key, :the_uuid)
- .exactly(27).times
201.times do
subject.increment!
@@ -82,6 +79,37 @@ RSpec.shared_examples 'housekeeps repository' do
expect(resource.pushes_since_gc).to eq(1)
end
+
+ context 'when optimized_repository feature flag is disabled' do
+ before do
+ stub_feature_flags(optimized_housekeeping: false)
+ end
+
+ it 'calls also the garbage collect worker with pack_refs every 6 commits' do
+ allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
+ allow(subject).to receive(:lease_key).and_return(:the_lease_key)
+
+ # At push 200
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :gc, :the_lease_key, :the_uuid)
+ .once
+ # At push 50, 100, 150
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :full_repack, :the_lease_key, :the_uuid)
+ .exactly(3).times
+ # At push 10, 20, ... (except those above)
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid)
+ .exactly(16).times
+ # At push 6, 12, 18, ... (except those above)
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :pack_refs, :the_lease_key, :the_uuid)
+ .exactly(27).times
+
+ 201.times do
+ subject.increment!
+ subject.execute if subject.needed?
+ end
+
+ expect(resource.pushes_since_gc).to eq(1)
+ end
+ end
end
it 'runs the task specifically requested' do
@@ -107,6 +135,17 @@ RSpec.shared_examples 'housekeeps repository' do
allow(resource).to receive(:pushes_since_gc).and_return(10)
expect(subject.needed?).to eq(true)
end
+
+ context 'when optimized_housekeeping is disabled' do
+ before do
+ stub_feature_flags(optimized_housekeeping: false)
+ end
+
+ it 'returns true pack refs is needed' do
+ allow(resource).to receive(:pushes_since_gc).and_return(described_class::PACK_REFS_PERIOD)
+ expect(subject.needed?).to eq(true)
+ end
+ end
end
describe '#increment!' do
diff --git a/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb b/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb
index 97304680316..acf15730180 100644
--- a/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb
+++ b/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb
@@ -11,7 +11,7 @@ RSpec.shared_examples 'moves repository shard in bulk' do
describe '#execute' do
it 'schedules container repository storage moves' do
expect { subject.execute(source_storage_name, destination_storage_name) }
- .to change(move_service_klass, :count).by(1)
+ .to change { move_service_klass.count }.by(1)
storage_move = container.repository_storage_moves.last!
@@ -29,7 +29,7 @@ RSpec.shared_examples 'moves repository shard in bulk' do
expect(subject).to receive(:log_info)
.with(/Container #{container.full_path} \(#{container.id}\) was skipped: #{container.class} is read-only/)
expect { subject.execute(source_storage_name, destination_storage_name) }
- .to change(move_service_klass, :count).by(0)
+ .to change { move_service_klass.count }.by(0)
end
end
end
diff --git a/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb
index 31919a4263d..e72e8e79411 100644
--- a/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb
@@ -7,5 +7,5 @@ RSpec.shared_examples 'issue_edit snowplow tracking' do
let(:namespace) { project.namespace }
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
- it_behaves_like 'Snowplow event tracking'
+ it_behaves_like 'Snowplow event tracking with RedisHLL context'
end
diff --git a/spec/support/shared_examples/services/timelogs/create_service_shared_examples.rb b/spec/support/shared_examples/services/timelogs/create_service_shared_examples.rb
index 53c42ec0e00..00d4224f021 100644
--- a/spec/support/shared_examples/services/timelogs/create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/timelogs/create_service_shared_examples.rb
@@ -1,6 +1,12 @@
# frozen_string_literal: true
RSpec.shared_examples 'issuable supports timelog creation service' do
+ let_it_be(:time_spent) { 3600 }
+ let_it_be(:spent_at) { Time.now }
+ let_it_be(:summary) { "Test summary" }
+
+ let(:service) { described_class.new(issuable, time_spent, spent_at, summary, user) }
+
shared_examples 'success_response' do
it 'sucessfully saves the timelog' do
is_expected.to be_success
@@ -9,7 +15,7 @@ RSpec.shared_examples 'issuable supports timelog creation service' do
expect(timelog).to be_persisted
expect(timelog.time_spent).to eq(time_spent)
- expect(timelog.spent_at).to eq('Fri, 08 Jul 2022 00:00:00.000000000 UTC +00:00')
+ expect(timelog.spent_at).to eq(spent_at)
expect(timelog.summary).to eq(summary)
expect(timelog.issuable).to eq(issuable)
end
@@ -34,6 +40,39 @@ RSpec.shared_examples 'issuable supports timelog creation service' do
users_container.add_reporter(user)
end
+ context 'when spent_at is in the future' do
+ let_it_be(:spent_at) { Time.now + 2.hours }
+
+ it 'returns an error' do
+ is_expected.to be_error
+
+ expect(subject.message).to eq("Spent at can't be a future date and time.")
+ expect(subject.http_status).to eq(404)
+ end
+ end
+
+ context 'when time_spent is zero' do
+ let_it_be(:time_spent) { 0 }
+
+ it 'returns an error' do
+ is_expected.to be_error
+
+ expect(subject.message).to eq("Time spent can't be zero.")
+ expect(subject.http_status).to eq(404)
+ end
+ end
+
+ context 'when time_spent is nil' do
+ let_it_be(:time_spent) { nil }
+
+ it 'returns an error' do
+ is_expected.to be_error
+
+ expect(subject.message).to eq("Time spent can't be blank")
+ expect(subject.http_status).to eq(404)
+ end
+ end
+
context 'when the timelog save fails' do
before do
allow_next_instance_of(Timelog) do |timelog|
@@ -54,6 +93,12 @@ RSpec.shared_examples 'issuable supports timelog creation service' do
end
RSpec.shared_examples 'issuable does not support timelog creation service' do
+ let_it_be(:time_spent) { 3600 }
+ let_it_be(:spent_at) { Time.now }
+ let_it_be(:summary) { "Test summary" }
+
+ let(:service) { described_class.new(issuable, time_spent, spent_at, summary, user) }
+
shared_examples 'error_response' do
it 'returns an error' do
is_expected.to be_error
diff --git a/spec/support/shared_examples/services/users/build_service_shared_examples.rb b/spec/support/shared_examples/services/users/build_service_shared_examples.rb
index 6a8695e1786..e448f2f874b 100644
--- a/spec/support/shared_examples/services/users/build_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/users/build_service_shared_examples.rb
@@ -84,9 +84,10 @@ RSpec.shared_examples_for 'current user not admin build items' do
end
end
- context 'when "send_user_confirmation_email" application setting is true' do
+ context 'when "email_confirmation_setting" application setting is set to `hard`' do
before do
- stub_application_setting(send_user_confirmation_email: true, signup_enabled?: true)
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
+ stub_application_setting(signup_enabled?: true)
end
it 'does not confirm the user' do
@@ -94,9 +95,10 @@ RSpec.shared_examples_for 'current user not admin build items' do
end
end
- context 'when "send_user_confirmation_email" application setting is false' do
+ context 'when "email_confirmation_setting" application setting is set to `off`' do
before do
- stub_application_setting(send_user_confirmation_email: false, signup_enabled?: true)
+ stub_application_setting_enum('email_confirmation_setting', 'off')
+ stub_application_setting(signup_enabled?: true)
end
it 'confirms the user' do
diff --git a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
index 980a752cf86..ced49b3e481 100644
--- a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
@@ -75,7 +75,7 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type|
end
it 'does not record the activity' do
- expect { service.execute }.not_to change(Event, :count)
+ expect { service.execute }.not_to change { Event.count }
end
it 'reports the error' do
diff --git a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
index fd10dd4367e..5511843e681 100644
--- a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
@@ -79,7 +79,7 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
end
it 'does not record the activity' do
- expect { service.execute page }.not_to change(Event, :count)
+ expect { service.execute page }.not_to change { Event.count }
end
it 'reports the error' do
diff --git a/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb b/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb
index ac17915c15a..ac064ed4c33 100644
--- a/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb
@@ -23,7 +23,7 @@ RSpec.shared_examples "setting work item's milestone" do
it "sets the work item's milestone" do
expect { execute_callback }
- .to change(work_item, :milestone)
+ .to change { work_item.milestone }
.from(nil)
.to(group_milestone)
end
@@ -34,7 +34,7 @@ RSpec.shared_examples "setting work item's milestone" do
it "sets the work item's milestone" do
expect { execute_callback }
- .to change(work_item, :milestone)
+ .to change { work_item.milestone }
.from(nil)
.to(project_milestone)
end
diff --git a/spec/support/shared_examples/work_item_base_types_importer.rb b/spec/support/shared_examples/work_item_base_types_importer.rb
index 593670ac4b8..b1011037584 100644
--- a/spec/support/shared_examples/work_item_base_types_importer.rb
+++ b/spec/support/shared_examples/work_item_base_types_importer.rb
@@ -4,7 +4,7 @@ RSpec.shared_examples 'work item base types importer' do
it "creates all base work item types if they don't exist" do
WorkItems::Type.delete_all
- expect { subject }.to change(WorkItems::Type, :count).from(0).to(WorkItems::Type::BASE_TYPES.count)
+ expect { subject }.to change { WorkItems::Type.count }.from(0).to(WorkItems::Type::BASE_TYPES.count)
types_in_db = WorkItems::Type.all.map { |type| type.slice(:base_type, :icon_name, :name).symbolize_keys }
expected_types = WorkItems::Type::BASE_TYPES.map do |type, attributes|
@@ -25,7 +25,7 @@ RSpec.shared_examples 'work item base types importer' do
subject
first_type.reload
end.to not_change(WorkItems::Type, :count).and(
- change(first_type, :name).from(original_name.upcase).to(original_name)
+ change { first_type.name }.from(original_name.upcase).to(original_name)
)
end
@@ -40,7 +40,7 @@ RSpec.shared_examples 'work item base types importer' do
it 'inserts all types and does nothing if some already existed' do
expect { subject }.to make_queries_matching(/INSERT/, 1).and(
- change(WorkItems::Type, :count).by(1)
+ change { WorkItems::Type.count }.by(1)
)
expect(WorkItems::Type.count).to eq(WorkItems::Type::BASE_TYPES.count)
end
diff --git a/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb b/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb
new file mode 100644
index 00000000000..b75aa27b2b7
--- /dev/null
+++ b/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'work item hierarchy restrictions importer' do
+ shared_examples_for 'adds restrictions' do
+ it "adds all restrictions if they don't exist" do
+ expect { subject }.to change { WorkItems::HierarchyRestriction.count }.from(0).to(4)
+ end
+ end
+
+ context 'when restrictions are missing' do
+ before do
+ WorkItems::HierarchyRestriction.delete_all
+ end
+
+ it_behaves_like 'adds restrictions'
+ end
+
+ context 'when base types are missing' do
+ before do
+ WorkItems::Type.delete_all
+ end
+
+ it_behaves_like 'adds restrictions'
+ end
+
+ context 'when restrictions already exist' do
+ before do
+ Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions
+ end
+
+ it 'upserts restrictions' do
+ restriction = WorkItems::HierarchyRestriction.first
+ depth = restriction.maximum_depth
+
+ restriction.update!(maximum_depth: depth + 1)
+
+ expect do
+ subject
+ restriction.reload
+ end.to not_change { WorkItems::HierarchyRestriction.count }.and(
+ change { restriction.maximum_depth }.from(depth + 1).to(depth)
+ )
+ end
+ end
+
+ context 'when some restrictions are missing' do
+ before do
+ Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions
+ WorkItems::HierarchyRestriction.limit(1).delete_all
+ end
+
+ it 'inserts missing restrictions and does nothing if some already existed' do
+ expect { subject }.to make_queries_matching(/INSERT/, 1).and(
+ change { WorkItems::HierarchyRestriction.count }.by(1)
+ )
+ expect(WorkItems::HierarchyRestriction.count).to eq(4)
+ end
+ end
+end
diff --git a/spec/workers/database/batched_background_migration/execution_worker_spec.rb b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb
index 9a850a98f2f..ae29b76ee87 100644
--- a/spec/workers/database/batched_background_migration/execution_worker_spec.rb
+++ b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb
@@ -1,11 +1,73 @@
# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Database::BatchedBackgroundMigration::ExecutionWorker, :clean_gitlab_redis_shared_state do
+RSpec.shared_examples 'batched background migrations execution worker' do
include ExclusiveLeaseHelpers
- describe '#perform' do
+ it 'is a limited capacity worker' do
+ expect(described_class.new).to be_a(LimitedCapacity::Worker)
+ end
+
+ describe 'defining the job attributes' do
+ it 'defines the data_consistency as always' do
+ expect(described_class.get_data_consistency).to eq(:always)
+ end
+
+ it 'defines the feature_category as database' do
+ expect(described_class.get_feature_category).to eq(:database)
+ end
+
+ it 'defines the idempotency as false' do
+ expect(described_class).not_to be_idempotent
+ end
+
+ it 'does not retry failed jobs' do
+ expect(described_class.sidekiq_options['retry']).to eq(0)
+ end
+
+ it 'does not deduplicate jobs' do
+ expect(described_class.get_deduplicate_strategy).to eq(:none)
+ end
+
+ it 'defines the queue namespace' do
+ expect(described_class.queue_namespace).to eq('batched_background_migrations')
+ end
+ end
+
+ describe '.perform_with_capacity' do
+ it 'enqueues jobs without modifying provided arguments' do
+ expect_next_instance_of(described_class) do |instance|
+ expect(instance).to receive(:remove_failed_jobs)
+ end
+
+ args = [['main', 123]]
+
+ expect(described_class)
+ .to receive(:bulk_perform_async)
+ .with(args)
+
+ described_class.perform_with_capacity(args)
+ end
+ end
+
+ describe '.max_running_jobs' do
+ it 'returns MAX_RUNNING_MIGRATIONS' do
+ expect(described_class.max_running_jobs).to eq(described_class::MAX_RUNNING_MIGRATIONS)
+ end
+ end
+
+ describe '#max_running_jobs' do
+ it 'returns MAX_RUNNING_MIGRATIONS' do
+ expect(described_class.new.max_running_jobs).to eq(described_class::MAX_RUNNING_MIGRATIONS)
+ end
+ end
+
+ describe '#remaining_work_count' do
+ it 'returns 0' do
+ expect(described_class.new.remaining_work_count).to eq(0)
+ end
+ end
+
+ describe '#perform_work' do
let(:database_name) { Gitlab::Database::MAIN_DATABASE_NAME.to_sym }
let(:base_model) { Gitlab::Database.database_base_models[database_name] }
let(:table_name) { :events }
@@ -28,7 +90,7 @@ RSpec.describe Database::BatchedBackgroundMigration::ExecutionWorker, :clean_git
expect(Gitlab::Database::BackgroundMigration::BatchedMigration).not_to receive(:find_executable)
expect(worker).not_to receive(:run_migration_job)
- worker.perform(database_name, migration.id)
+ worker.perform_work(database_name, migration.id)
end
end
@@ -50,7 +112,7 @@ RSpec.describe Database::BatchedBackgroundMigration::ExecutionWorker, :clean_git
expect(Gitlab::Database::BackgroundMigration::BatchedMigration).not_to receive(:find_executable)
expect(worker).not_to receive(:run_migration_job)
- worker.perform(:ci, 123)
+ worker.perform_work(:ci, 123)
end
end
@@ -58,7 +120,7 @@ RSpec.describe Database::BatchedBackgroundMigration::ExecutionWorker, :clean_git
it 'does nothing' do
expect(worker).not_to receive(:run_migration_job)
- worker.perform(database_name, non_existing_record_id)
+ worker.perform_work(database_name, non_existing_record_id)
end
end
@@ -81,7 +143,7 @@ RSpec.describe Database::BatchedBackgroundMigration::ExecutionWorker, :clean_git
expect(worker).not_to receive(:run_migration_job)
- worker.perform(database_name, migration.id)
+ worker.perform_work(database_name, migration.id)
end
end
@@ -91,7 +153,7 @@ RSpec.describe Database::BatchedBackgroundMigration::ExecutionWorker, :clean_git
expect(migration).to receive(:interval_elapsed?).with(variance: interval_variance).and_return(false)
expect(worker).not_to receive(:run_migration_job)
- worker.perform(database_name, migration.id)
+ worker.perform_work(database_name, migration.id)
end
end
@@ -107,7 +169,7 @@ RSpec.describe Database::BatchedBackgroundMigration::ExecutionWorker, :clean_git
expect(worker).not_to receive(:run_migration_job)
- worker.perform(database_name, migration.id)
+ worker.perform_work(database_name, migration.id)
end
end
@@ -117,7 +179,7 @@ RSpec.describe Database::BatchedBackgroundMigration::ExecutionWorker, :clean_git
expect(worker).to receive(:run_migration_job).and_raise(RuntimeError, 'I broke')
- expect { worker.perform(database_name, migration.id) }.to raise_error(RuntimeError, 'I broke')
+ expect { worker.perform_work(database_name, migration.id) }.to raise_error(RuntimeError, 'I broke')
end
it 'runs the migration' do
@@ -132,7 +194,7 @@ RSpec.describe Database::BatchedBackgroundMigration::ExecutionWorker, :clean_git
expect(worker).to receive(:run_migration_job).and_call_original
- worker.perform(database_name, migration.id)
+ worker.perform_work(database_name, migration.id)
end
end
end
diff --git a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
index 0be55fd2a3e..09ebc495e61 100644
--- a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
+++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
@@ -125,10 +125,28 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
context 'when no active migrations exist' do
- it 'does nothing' do
- expect(worker).not_to receive(:run_active_migration)
+ context 'when parallel execution is disabled' do
+ before do
+ stub_feature_flags(batched_migrations_parallel_execution: false)
+ end
- worker.perform
+ it 'does nothing' do
+ expect(worker).not_to receive(:run_active_migration)
+
+ worker.perform
+ end
+ end
+
+ context 'when parallel execution is enabled' do
+ before do
+ stub_feature_flags(batched_migrations_parallel_execution: true)
+ end
+
+ it 'does nothing' do
+ expect(worker).not_to receive(:queue_migrations_for_execution)
+
+ worker.perform
+ end
end
end
@@ -136,7 +154,6 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
let(:job_interval) { 5.minutes }
let(:lease_timeout) { 15.minutes }
let(:lease_key) { described_class.name.demodulize.underscore }
- let(:interval_variance) { described_class::INTERVAL_VARIANCE }
let(:migration_id) { 123 }
let(:migration) do
build(
@@ -145,52 +162,86 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
)
end
+ let(:execution_worker_class) do
+ case tracking_database
+ when :main
+ Database::BatchedBackgroundMigration::MainExecutionWorker
+ when :ci
+ Database::BatchedBackgroundMigration::CiExecutionWorker
+ end
+ end
+
before do
allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration)
.with(connection: base_model.connection)
.and_return(migration)
-
- allow(migration).to receive(:interval_elapsed?).with(variance: interval_variance).and_return(true)
- allow(migration).to receive(:reload)
end
- context 'when the calculated timeout is less than the minimum allowed' do
- let(:minimum_timeout) { described_class::MINIMUM_LEASE_TIMEOUT }
- let(:job_interval) { 2.minutes }
+ context 'when parallel execution is disabled' do
+ before do
+ stub_feature_flags(batched_migrations_parallel_execution: false)
+ end
+
+ let(:execution_worker) { instance_double(execution_worker_class) }
- it 'sets the lease timeout to the minimum value' do
- expect_to_obtain_exclusive_lease(lease_key, timeout: minimum_timeout)
+ context 'when the calculated timeout is less than the minimum allowed' do
+ let(:minimum_timeout) { described_class::MINIMUM_LEASE_TIMEOUT }
+ let(:job_interval) { 2.minutes }
- expect_next_instance_of(Database::BatchedBackgroundMigration::ExecutionWorker) do |worker|
- expect(worker).to receive(:perform).with(tracking_database, migration_id)
+ it 'sets the lease timeout to the minimum value' do
+ expect_to_obtain_exclusive_lease(lease_key, timeout: minimum_timeout)
+
+ expect(execution_worker_class).to receive(:new).and_return(execution_worker)
+ expect(execution_worker).to receive(:perform_work).with(tracking_database, migration_id)
+
+ expect(worker).to receive(:run_active_migration).and_call_original
+
+ worker.perform
end
+ end
- expect(worker).to receive(:run_active_migration).and_call_original
+ it 'always cleans up the exclusive lease' do
+ lease = stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
+
+ expect(lease).to receive(:try_obtain).and_return(true)
+
+ expect(worker).to receive(:run_active_migration).and_raise(RuntimeError, 'I broke')
+ expect(lease).to receive(:cancel)
+
+ expect { worker.perform }.to raise_error(RuntimeError, 'I broke')
+ end
+
+ it 'delegetes the execution to ExecutionWorker' do
+ base_model = Gitlab::Database.database_base_models[tracking_database]
+
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(base_model.connection).and_yield
+ expect(execution_worker_class).to receive(:new).and_return(execution_worker)
+ expect(execution_worker).to receive(:perform_work).with(tracking_database, migration_id)
worker.perform
end
end
- it 'always cleans up the exclusive lease' do
- lease = stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
+ context 'when parallel execution is enabled' do
+ before do
+ stub_feature_flags(batched_migrations_parallel_execution: true)
+ end
- expect(lease).to receive(:try_obtain).and_return(true)
+ it 'delegetes the execution to ExecutionWorker' do
+ expect(Gitlab::Database::BackgroundMigration::BatchedMigration)
+ .to receive(:active_migrations_distinct_on_table).with(
+ connection: base_model.connection,
+ limit: execution_worker_class.max_running_jobs
+ ).and_return([migration])
- expect(worker).to receive(:run_active_migration).and_raise(RuntimeError, 'I broke')
- expect(lease).to receive(:cancel)
+ expected_arguments = [
+ [tracking_database.to_s, migration_id]
+ ]
- expect { worker.perform }.to raise_error(RuntimeError, 'I broke')
- end
+ expect(execution_worker_class).to receive(:perform_with_capacity).with(expected_arguments)
- it 'delegetes the execution to ExecutionWorker' do
- base_model = Gitlab::Database.database_base_models[tracking_database]
-
- expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(base_model.connection).and_yield
- expect_next_instance_of(Database::BatchedBackgroundMigration::ExecutionWorker) do |worker|
- expect(worker).to receive(:perform).with(tracking_database, migration_id)
+ worker.perform
end
-
- worker.perform
end
end
end
@@ -249,6 +300,8 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
before do
+ stub_feature_flags(execute_batched_migrations_on_schedule: true)
+
# Create example table populated with test data to migrate.
#
# Test data should have two records that won't be updated:
@@ -269,80 +322,96 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
WHERE some_column = #{migration_records - 5};
SQL
- stub_feature_flags(execute_batched_migrations_on_schedule: true)
-
stub_const('Gitlab::BackgroundMigration::ExampleDataMigration', migration_class)
end
- subject(:full_migration_run) do
- # process all batches, then do an extra execution to mark the job as finished
- (number_of_batches + 1).times do
- described_class.new.perform
+ shared_examples 'batched background migration execution' do
+ subject(:full_migration_run) do
+ # process all batches, then do an extra execution to mark the job as finished
+ (number_of_batches + 1).times do
+ described_class.new.perform
- travel_to((migration.interval + described_class::INTERVAL_VARIANCE).seconds.from_now)
+ travel_to((migration.interval + described_class::INTERVAL_VARIANCE).seconds.from_now)
+ end
end
- end
- it 'marks the migration record as finished' do
- expect { full_migration_run }.to change { migration.reload.status }.from(1).to(3) # active -> finished
- end
+ it 'marks the migration record as finished' do
+ expect { full_migration_run }.to change { migration.reload.status }.from(1).to(3) # active -> finished
+ end
- it 'creates job records for each processed batch', :aggregate_failures do
- expect { full_migration_run }.to change { migration.reload.batched_jobs.count }.from(0)
+ it 'creates job records for each processed batch', :aggregate_failures do
+ expect { full_migration_run }.to change { migration.reload.batched_jobs.count }.from(0)
- final_min_value = migration.batched_jobs.reduce(1) do |next_min_value, batched_job|
- expect(batched_job.min_value).to eq(next_min_value)
+ final_min_value = migration.batched_jobs.order(id: :asc).reduce(1) do |next_min_value, batched_job|
+ expect(batched_job.min_value).to eq(next_min_value)
- batched_job.max_value + 1
+ batched_job.max_value + 1
+ end
+
+ final_max_value = final_min_value - 1
+ expect(final_max_value).to eq(migration_records)
end
- final_max_value = final_min_value - 1
- expect(final_max_value).to eq(migration_records)
- end
+ it 'marks all job records as succeeded', :aggregate_failures do
+ expect { full_migration_run }.to change { migration.reload.batched_jobs.count }.from(0)
- it 'marks all job records as succeeded', :aggregate_failures do
- expect { full_migration_run }.to change { migration.reload.batched_jobs.count }.from(0)
+ expect(migration.batched_jobs).to all(be_succeeded)
+ end
- expect(migration.batched_jobs).to all(be_succeeded)
- end
+ it 'updates matching records in the range', :aggregate_failures do
+ expect { full_migration_run }
+ .to change { example_data.where('status = 1 AND some_column <> 0').count }
+ .from(migration_records).to(1)
- it 'updates matching records in the range', :aggregate_failures do
- expect { full_migration_run }
- .to change { example_data.where('status = 1 AND some_column <> 0').count }
- .from(migration_records).to(1)
+ record_outside_range = example_data.last
- record_outside_range = example_data.last
+ expect(record_outside_range.status).to eq(1)
+ expect(record_outside_range.some_column).not_to eq(0)
+ end
- expect(record_outside_range.status).to eq(1)
- expect(record_outside_range.some_column).not_to eq(0)
- end
+ it 'does not update non-matching records in the range' do
+ expect { full_migration_run }.not_to change { example_data.where('status <> 1 AND some_column <> 0').count }
+ end
- it 'does not update non-matching records in the range' do
- expect { full_migration_run }.not_to change { example_data.where('status <> 1 AND some_column <> 0').count }
- end
+ context 'health status' do
+ subject(:migration_run) { described_class.new.perform }
+
+ it 'puts migration on hold when there is autovaccum activity on related tables' do
+ swapout_view_for_table(:postgres_autovacuum_activity, connection: connection)
+ create(
+ :postgres_autovacuum_activity,
+ table: migration.table_name,
+ table_identifier: "public.#{migration.table_name}"
+ )
+
+ expect { migration_run }.to change { migration.reload.on_hold? }.from(false).to(true)
+ end
- context 'health status' do
- subject(:migration_run) { described_class.new.perform }
+ it 'puts migration on hold when the pending WAL count is above the limit' do
+ sql = Gitlab::Database::BackgroundMigration::HealthStatus::Indicators::WriteAheadLog::PENDING_WAL_COUNT_SQL
+ limit = Gitlab::Database::BackgroundMigration::HealthStatus::Indicators::WriteAheadLog::LIMIT
- it 'puts migration on hold when there is autovaccum activity on related tables' do
- swapout_view_for_table(:postgres_autovacuum_activity, connection: connection)
- create(
- :postgres_autovacuum_activity,
- table: migration.table_name,
- table_identifier: "public.#{migration.table_name}"
- )
+ expect(connection).to receive(:execute).with(sql).and_return([{ 'pending_wal_count' => limit + 1 }])
- expect { migration_run }.to change { migration.reload.on_hold? }.from(false).to(true)
+ expect { migration_run }.to change { migration.reload.on_hold? }.from(false).to(true)
+ end
end
+ end
- it 'puts migration on hold when the pending WAL count is above the limit' do
- sql = Gitlab::Database::BackgroundMigration::HealthStatus::Indicators::WriteAheadLog::PENDING_WAL_COUNT_SQL
- limit = Gitlab::Database::BackgroundMigration::HealthStatus::Indicators::WriteAheadLog::LIMIT
+ context 'when parallel execution is disabled' do
+ before do
+ stub_feature_flags(batched_migrations_parallel_execution: false)
+ end
- expect(connection).to receive(:execute).with(sql).and_return([{ 'pending_wal_count' => limit + 1 }])
+ it_behaves_like 'batched background migration execution'
+ end
- expect { migration_run }.to change { migration.reload.on_hold? }.from(false).to(true)
+ context 'when parallel execution is enabled', :sidekiq_inline do
+ before do
+ stub_feature_flags(batched_migrations_parallel_execution: true)
end
+
+ it_behaves_like 'batched background migration execution'
end
end
end
diff --git a/spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb b/spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb
index 465aca63148..6707f65eb69 100644
--- a/spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb
+++ b/spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb
@@ -15,7 +15,7 @@ RSpec.shared_examples 'schedules bulk repository shard moves' do
let(:job_args) { [source_storage_name, destination_storage_name] }
it 'schedules container repository storage moves' do
- expect { subject }.to change(move_service_klass, :count).by(1)
+ expect { subject }.to change { move_service_klass.count }.by(1)
storage_move = container.repository_storage_moves.last!
diff --git a/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb b/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb
index babd7cfbbeb..c50dc6d5372 100644
--- a/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb
+++ b/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb
@@ -15,7 +15,7 @@ RSpec.shared_examples 'an update storage move worker' do
expect do
subject.perform(container.id, 'test_second_storage')
- end.to change(repository_storage_move_klass, :count).by(1)
+ end.to change { repository_storage_move_klass.count }.by(1)
storage_move = container.repository_storage_moves.last
expect(storage_move).to have_attributes(
@@ -32,7 +32,7 @@ RSpec.shared_examples 'an update storage move worker' do
expect do
subject.perform(nil, nil, repository_storage_move.id)
- end.not_to change(repository_storage_move_klass, :count)
+ end.not_to change { repository_storage_move_klass.count }
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 a6f5b3862a2..0e79e32b78a 100644
--- a/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb
+++ b/spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb
@@ -275,7 +275,7 @@ RSpec.describe ExceedQueryLimitHelpers do
expect(test_matcher.log_message)
.to match(%r{ORDER BY.*#{TestQueries.table_name}.*LIMIT 1})
expect(test_matcher.log_message)
- .not_to match(%r{\/\*.*correlation_id.*\*\/})
+ .not_to match(%r{/\*.*correlation_id.*\*/})
end
end
end
diff --git a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
index ebea644bbf0..e3155d3c377 100644
--- a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
+++ b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
@@ -19,6 +19,30 @@ RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_r
let(:main_connection) { ApplicationRecord.connection }
let(:ci_connection) { Ci::ApplicationRecord.connection }
+ let(:detached_partition_table) { '_test_gitlab_main_part_20220101' }
+
+ before do
+ create_detached_partition_sql = <<~SQL
+ CREATE TABLE IF NOT EXISTS gitlab_partitions_dynamic._test_gitlab_main_part_20220101 (
+ id bigserial primary key not null
+ )
+ SQL
+
+ main_connection.execute(create_detached_partition_sql)
+ ci_connection.execute(create_detached_partition_sql)
+
+ Gitlab::Database::SharedModel.using_connection(main_connection) do
+ Postgresql::DetachedPartition.create!(
+ table_name: detached_partition_table,
+ drop_after: Time.current
+ )
+ end
+
+ allow(Gitlab::Database::GitlabSchema).to receive(:table_schema).and_call_original
+ allow(Gitlab::Database::GitlabSchema).to receive(:table_schema)
+ .with(detached_partition_table).and_return(:gitlab_main)
+ end
+
context 'single database' do
before do
skip_if_multiple_databases_are_setup
@@ -46,6 +70,13 @@ RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_r
context 'multiple databases' do
before do
skip_if_multiple_databases_not_setup
+
+ Gitlab::Database::SharedModel.using_connection(ci_connection) do
+ Postgresql::DetachedPartition.create!(
+ table_name: detached_partition_table,
+ drop_after: Time.current
+ )
+ end
end
context 'when locking writes' do
@@ -87,6 +118,13 @@ RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_r
main_connection.execute("truncate ci_build_needs")
end.to raise_error(ActiveRecord::StatementInvalid, /Table: "ci_build_needs" is write protected/)
end
+
+ it 'prevents writes to detached partitions' do
+ run_rake_task('gitlab:db:lock_writes')
+ expect do
+ ci_connection.execute("INSERT INTO gitlab_partitions_dynamic.#{detached_partition_table} DEFAULT VALUES")
+ end.to raise_error(ActiveRecord::StatementInvalid, /Table: "#{detached_partition_table}" is write protected/)
+ end
end
context 'when running in dry_run mode' do
@@ -138,6 +176,14 @@ RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_r
main_connection.execute("delete from ci_builds")
end.not_to raise_error
end
+
+ it 'allows writes again to detached partitions' do
+ run_rake_task('gitlab:db:unlock_writes')
+
+ expect do
+ ci_connection.execute("INSERT INTO gitlab_partitions_dynamic._test_gitlab_main_part_20220101 DEFAULT VALUES")
+ end.not_to raise_error
+ end
end
end
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 08bec9fda78..22abfc33d1b 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -701,6 +701,16 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
describe '#up' do
subject { run_rake_task("gitlab:db:migration_testing:up:#{db}") }
+ let(:migrations_id_runner) do
+ instance_double('Gitlab::Database::Migrations::BatchedMigrationLastId', store: true)
+ end
+
+ before do
+ allow(::Gitlab::Database::Migrations::Runner).to(
+ receive(:batched_migrations_last_id).and_return(migrations_id_runner)
+ )
+ end
+
it 'delegates to the migration runner' do
expect(::Gitlab::Database::Migrations::Runner).to receive(:up).with(database: db).and_return(runner)
expect(runner).to receive(:run)
diff --git a/spec/tasks/gitlab/feature_categories_rake_spec.rb b/spec/tasks/gitlab/feature_categories_rake_spec.rb
new file mode 100644
index 00000000000..22f36309a7c
--- /dev/null
+++ b/spec/tasks/gitlab/feature_categories_rake_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'gitlab:feature_categories:index', :silence_stdout, feature_category: :scalability do
+ before do
+ Rake.application.rake_require 'tasks/gitlab/feature_categories'
+ end
+
+ 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(
+ 'authentication_and_authorization' => a_collection_including(
+ klass: 'API::AccessRequests',
+ action: '/groups/:id/access_requests',
+ source_location: [
+ 'lib/api/access_requests.rb',
+ an_instance_of(Integer)
+ ]
+ )
+ ),
+ 'sidekiq_workers' => a_hash_including(
+ 'source_code_management' => a_collection_including(
+ klass: 'MergeWorker',
+ source_location: [
+ 'app/workers/merge_worker.rb',
+ an_instance_of(Integer)
+ ]
+ )
+ ),
+ 'database_tables' => a_hash_including(
+ 'container_scanning' => a_collection_including('vulnerability_advisories')
+ )
+ }
+
+ expect(YAML).to receive(:dump).with(a_hash_including(expected))
+
+ run_rake_task('gitlab:feature_categories:index')
+ end
+end
diff --git a/spec/tasks/gitlab/lfs/migrate_rake_spec.rb b/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
index 3b571507bac..bc3113c2926 100644
--- a/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'gitlab:lfs namespace rake task', :silence_stdout do
let(:remote) { ObjectStorage::Store::REMOTE }
before do
- stub_lfs_object_storage(background_upload: false, direct_upload: false)
+ stub_lfs_object_storage(direct_upload: false)
end
describe 'migrate' do
@@ -43,7 +43,7 @@ RSpec.describe 'gitlab:lfs namespace rake task', :silence_stdout do
let(:lfs_object) { create(:lfs_object, :with_file, :object_storage) }
before do
- stub_lfs_object_storage(background_upload: false, direct_upload: true)
+ stub_lfs_object_storage(direct_upload: true)
end
context 'object storage enabled' do
diff --git a/spec/tasks/gitlab/refresh_project_statistics_build_artifacts_size_rake_spec.rb b/spec/tasks/gitlab/refresh_project_statistics_build_artifacts_size_rake_spec.rb
index 3495b535cff..3ee01977cba 100644
--- a/spec/tasks/gitlab/refresh_project_statistics_build_artifacts_size_rake_spec.rb
+++ b/spec/tasks/gitlab/refresh_project_statistics_build_artifacts_size_rake_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'gitlab:refresh_project_statistics_build_artifacts_size rake task
end
it 'inserts refreshes in batches with a sleep' do
- expect(Projects::BuildArtifactsSizeRefresh).to receive(:enqueue_refresh).with([project_1, project_2]).ordered
+ expect(Projects::BuildArtifactsSizeRefresh).to receive(:enqueue_refresh).with(match_array([project_1, project_2])).ordered
expect(Kernel).to receive(:sleep).with(1)
expect(Projects::BuildArtifactsSizeRefresh).to receive(:enqueue_refresh).with([project_3]).ordered
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index 52a9738fb51..195859eac70 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -22,4 +22,23 @@ RSpec.describe 'gitlab:shell rake tasks', :silence_stdout do
run_rake_task('gitlab:shell:install')
end
end
+
+ describe 'setup task' do
+ it 'writes authorized keys into the file' do
+ allow(Gitlab::CurrentSettings).to receive(:authorized_keys_enabled?).and_return(true)
+ stub_env('force', 'yes')
+
+ auth_key = create(:key)
+ auth_and_signing_key = create(:key, usage_type: :auth_and_signing)
+ create(:key, usage_type: :signing)
+
+ expect_next_instance_of(Gitlab::AuthorizedKeys) do |instance|
+ expect(instance).to receive(:batch_add_keys).once do |keys|
+ expect(keys).to match_array([auth_key, auth_and_signing_key])
+ end
+ end
+
+ run_rake_task('gitlab:shell:setup')
+ end
+ end
end
diff --git a/spec/tasks/gitlab/update_templates_rake_spec.rb b/spec/tasks/gitlab/update_templates_rake_spec.rb
index 85da490f718..47eeea239ea 100644
--- a/spec/tasks/gitlab/update_templates_rake_spec.rb
+++ b/spec/tasks/gitlab/update_templates_rake_spec.rb
@@ -2,13 +2,14 @@
require 'rake_helper'
-RSpec.describe 'gitlab:update_project_templates rake task', :silence_stdout do
+RSpec.describe 'gitlab:update_project_templates rake task', :silence_stdout, feature_category: :importers do
let!(:tmpdir) { Dir.mktmpdir }
let(:template) { Gitlab::ProjectTemplate.find(:rails) }
before do
Rake.application.rake_require 'tasks/gitlab/update_templates'
- create(:admin)
+ admin = create(:admin)
+ create(:key, user: admin)
allow(Gitlab::ProjectTemplate)
.to receive(:archive_directory)
@@ -28,6 +29,9 @@ RSpec.describe 'gitlab:update_project_templates rake task', :silence_stdout do
end
it 'updates valid project templates' do
+ expect(Gitlab::TaskHelpers).to receive(:run_command!).with(anything).exactly(6).times.and_call_original
+ expect(Gitlab::TaskHelpers).to receive(:run_command!).with(%w[git push -u origin master])
+
expect { run_rake_task('gitlab:update_project_templates', [template.name]) }
.to change { Dir.entries(tmpdir) }
.by(["#{template.name}.tar.gz"])
diff --git a/spec/tasks/gitlab/usage_data_rake_spec.rb b/spec/tasks/gitlab/usage_data_rake_spec.rb
index 7ddba4ceb9b..95ebaf6ea24 100644
--- a/spec/tasks/gitlab/usage_data_rake_spec.rb
+++ b/spec/tasks/gitlab/usage_data_rake_spec.rb
@@ -70,8 +70,15 @@ RSpec.describe 'gitlab:usage data take tasks', :silence_stdout do
end
describe 'generate_ci_template_events' do
- it "generates #{Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH}" do
+ around do |example|
FileUtils.rm_rf(Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH)
+
+ example.run
+
+ `git checkout -- #{Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH}`
+ end
+
+ it "generates #{Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH}" do
run_rake_task('gitlab:usage_data:generate_ci_template_events')
expect(File.exist?(Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH)).to be true
@@ -80,7 +87,7 @@ RSpec.describe 'gitlab:usage data take tasks', :silence_stdout do
private
- def stub_response(url: service_ping_payload_url, body:, status: 201)
+ def stub_response(body:, url: service_ping_payload_url, status: 201)
stub_full_request(url, method: :post)
.to_return(
headers: { 'Content-Type' => 'application/json' },
diff --git a/spec/tooling/danger/feature_flag_spec.rb b/spec/tooling/danger/feature_flag_spec.rb
index 7cae3e0a8b3..0e9eda54510 100644
--- a/spec/tooling/danger/feature_flag_spec.rb
+++ b/spec/tooling/danger/feature_flag_spec.rb
@@ -135,7 +135,7 @@ RSpec.describe Tooling::Danger::FeatureFlag do
end
context 'when MR labels does not match FF group' do
- let(:mr_group_label) { 'group::access' }
+ let(:mr_group_label) { 'group::authentication and authorization' }
specify { expect(result).to eq(false) }
end
diff --git a/spec/tooling/danger/product_intelligence_spec.rb b/spec/tooling/danger/product_intelligence_spec.rb
index ea08e3bc6db..fab8b0c61fa 100644
--- a/spec/tooling/danger/product_intelligence_spec.rb
+++ b/spec/tooling/danger/product_intelligence_spec.rb
@@ -12,12 +12,16 @@ RSpec.describe Tooling::Danger::ProductIntelligence do
subject(:product_intelligence) { fake_danger.new(helper: fake_helper) }
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
- let(:changed_files) { ['metrics/counts_7d/test_metric.yml'] }
- let(:changed_lines) { ['+tier: ee'] }
+ let(:previous_label_to_add) { 'label_to_add' }
+ let(:labels_to_add) { [previous_label_to_add] }
+ let(:ci_env) { true }
+ let(:has_product_intelligence_label) { true }
before do
- allow(fake_helper).to receive(:all_changed_files).and_return(changed_files)
allow(fake_helper).to receive(:changed_lines).and_return(changed_lines)
+ allow(fake_helper).to receive(:labels_to_add).and_return(labels_to_add)
+ allow(fake_helper).to receive(:ci?).and_return(ci_env)
+ allow(fake_helper).to receive(:mr_has_labels?).with('product intelligence').and_return(has_product_intelligence_label)
end
describe '#check!' do
@@ -26,17 +30,13 @@ RSpec.describe Tooling::Danger::ProductIntelligence do
let(:markdown_formatted_list) { 'markdown formatted list' }
let(:review_pending_label) { 'product intelligence::review pending' }
let(:approved_label) { 'product intelligence::approved' }
- let(:ci_env) { true }
- let(:previous_label_to_add) { 'label_to_add' }
- let(:labels_to_add) { [previous_label_to_add] }
- let(:has_product_intelligence_label) { true }
+ let(:changed_files) { ['metrics/counts_7d/test_metric.yml'] }
+ let(:changed_lines) { ['+tier: ee'] }
before do
+ allow(fake_helper).to receive(:all_changed_files).and_return(changed_files)
allow(fake_helper).to receive(:changes_by_category).and_return(product_intelligence: changed_files, database: ['other_files.yml'])
- allow(fake_helper).to receive(:ci?).and_return(ci_env)
- allow(fake_helper).to receive(:mr_has_labels?).with('product intelligence').and_return(has_product_intelligence_label)
allow(fake_helper).to receive(:markdown_list).with(changed_files).and_return(markdown_formatted_list)
- allow(fake_helper).to receive(:labels_to_add).and_return(labels_to_add)
end
shared_examples "doesn't add new labels" do
@@ -121,4 +121,58 @@ RSpec.describe Tooling::Danger::ProductIntelligence do
end
end
end
+
+ describe '#check_affected_scopes!' do
+ let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'tooling', 'fixtures', 'metrics', '*.rb')) }
+ let(:changed_lines) { ['+ scope :active, -> { iwhere(email: Array(emails)) }'] }
+
+ before do
+ allow(Dir).to receive(:glob).and_return(fixture_dir_glob)
+ allow(fake_helper).to receive(:markdown_list).with({ 'active' => fixture_dir_glob }).and_return('a')
+ end
+
+ context 'when a model was modified' do
+ let(:modified_files) { ['app/models/super_user.rb'] }
+
+ context 'when a scope is changed' do
+ context 'and a metrics uses the affected scope' do
+ it 'producing warning' do
+ expect(product_intelligence).to receive(:warn).with(%r{#{modified_files}})
+
+ product_intelligence.check_affected_scopes!
+ end
+ end
+
+ context 'when no metrics using the affected scope' do
+ let(:changed_lines) { ['+scope :foo, -> { iwhere(email: Array(emails)) }'] }
+
+ it 'doesnt do anything' do
+ expect(product_intelligence).not_to receive(:warn)
+
+ product_intelligence.check_affected_scopes!
+ end
+ end
+ end
+ end
+
+ context 'when an unrelated model with matching scope was modified' do
+ let(:modified_files) { ['app/models/post_box.rb'] }
+
+ it 'doesnt do anything' do
+ expect(product_intelligence).not_to receive(:warn)
+
+ product_intelligence.check_affected_scopes!
+ end
+ end
+
+ context 'when models arent modified' do
+ let(:modified_files) { ['spec/app/models/user_spec.rb'] }
+
+ it 'doesnt do anything' do
+ expect(product_intelligence).not_to receive(:warn)
+
+ product_intelligence.check_affected_scopes!
+ end
+ end
+ end
end
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index f9ad9ed13c2..669867ffb4f 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -156,8 +156,6 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'lib/gitlab/database.rb' | [:database, :backend]
'lib/gitlab/database/foo' | [:database, :backend]
'ee/lib/gitlab/database/foo' | [:database, :backend]
- 'lib/gitlab/github_import.rb' | [:database, :backend]
- 'lib/gitlab/github_import/foo' | [:database, :backend]
'lib/gitlab/sql/foo' | [:database, :backend]
'rubocop/cop/migration/foo' | [:database]
diff --git a/spec/tooling/danger/specs_spec.rb b/spec/tooling/danger/specs_spec.rb
index d6aed86e7dc..dcc1f592062 100644
--- a/spec/tooling/danger/specs_spec.rb
+++ b/spec/tooling/danger/specs_spec.rb
@@ -9,7 +9,7 @@ require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/specs'
require_relative '../../../tooling/danger/project_helper'
-RSpec.describe Tooling::Danger::Specs do
+RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do
include_context "with dangerfile"
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
@@ -217,4 +217,68 @@ RSpec.describe Tooling::Danger::Specs do
specs.add_suggestions_for_project_factory_usage(filename)
end
end
+
+ describe '#add_suggestions_for_feature_category' do
+ let(:template) do
+ <<~SUGGESTION_MARKDOWN
+ ```suggestion
+ %<suggested_line>s
+ ```
+
+ Consider adding `feature_category: <feature_category_name>` for this example if it is not set already.
+ See [testing best practices](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#feature-category-metadata).
+ SUGGESTION_MARKDOWN
+ end
+
+ let(:file_lines) do
+ [
+ " require 'spec_helper'",
+ " \n",
+ " RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController, feature_category: :planning_analytics do",
+ " end",
+ "RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do",
+ " let_it_be(:user) { create(:user) }",
+ " end",
+ " describe 'GET \"time_summary\"' do",
+ " end",
+ " RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do",
+ " let_it_be(:user) { create(:user) }",
+ " end",
+ " describe 'GET \"time_summary\"' do",
+ " end"
+ ]
+ end
+
+ let(:matching_lines) do
+ [
+ "+ RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController, feature_category: :planning_analytics do",
+ "+RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do",
+ "+ RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do"
+ ]
+ end
+
+ let(:changed_lines) do
+ [
+ "+ RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController, feature_category: :planning_analytics do",
+ "+RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do",
+ "+ let_it_be(:user) { create(:user) }",
+ "- end",
+ "+ describe 'GET \"time_summary\"' do",
+ "+ RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do"
+ ]
+ end
+
+ it 'adds suggestions at the correct lines', :aggregate_failures do
+ [
+ { suggested_line: "RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do", number: 5 },
+ { suggested_line: " RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do", number: 10 }
+
+ ].each do |test_case|
+ comment = format(template, suggested_line: test_case[:suggested_line])
+ expect(specs).to receive(:markdown).with(comment, file: filename, line: test_case[:number])
+ end
+
+ specs.add_suggestions_for_feature_category(filename)
+ end
+ end
end
diff --git a/spec/tooling/danger/stable_branch_spec.rb b/spec/tooling/danger/stable_branch_spec.rb
new file mode 100644
index 00000000000..08fd25b30e0
--- /dev/null
+++ b/spec/tooling/danger/stable_branch_spec.rb
@@ -0,0 +1,169 @@
+# frozen_string_literal: true
+
+require 'gitlab-dangerfiles'
+require 'gitlab/dangerfiles/spec_helper'
+require 'rspec-parameterized'
+require 'httparty'
+
+require_relative '../../../tooling/danger/stable_branch'
+
+RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
+ using RSpec::Parameterized::TableSyntax
+
+ include_context 'with dangerfile'
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+
+ let(:stable_branch) { fake_danger.new(helper: fake_helper) }
+
+ describe '#check!' do
+ subject { stable_branch.check! }
+
+ shared_examples 'without a failure' do
+ it 'does not add a failure' do
+ expect(stable_branch).not_to receive(:fail)
+
+ subject
+ end
+ end
+
+ shared_examples 'with a failure' do |failure_message|
+ it 'fails' do
+ expect(stable_branch).to receive(:fail).with(failure_message)
+
+ subject
+ end
+ end
+
+ context 'when not applicable' do
+ where(:stable_branch?, :security_mr?) do
+ true | true
+ false | true
+ false | false
+ end
+
+ with_them do
+ before do
+ allow(fake_helper).to receive(:mr_target_branch).and_return(stable_branch? ? '15-1-stable-ee' : 'main')
+ allow(fake_helper).to receive(:security_mr?).and_return(security_mr?)
+ end
+
+ it_behaves_like "without a failure"
+ end
+ end
+
+ context 'when applicable' do
+ let(:target_branch) { '15-1-stable-ee' }
+ let(:feature_label_present) { false }
+ let(:bug_label_present) { true }
+ let(:response_success) { true }
+ let(:parsed_response) do
+ [
+ { 'version' => '15.1.1' },
+ { 'version' => '15.1.0' },
+ { 'version' => '15.0.2' },
+ { 'version' => '15.0.1' },
+ { 'version' => '15.0.0' },
+ { 'version' => '14.10.3' },
+ { 'version' => '14.10.2' },
+ { 'version' => '14.9.3' }
+ ]
+ end
+
+ let(:version_response) do
+ instance_double(
+ HTTParty::Response,
+ success?: response_success,
+ parsed_response: parsed_response
+ )
+ end
+
+ before do
+ allow(fake_helper).to receive(:mr_target_branch).and_return(target_branch)
+ allow(fake_helper).to receive(:security_mr?).and_return(false)
+ allow(fake_helper).to receive(:mr_has_labels?).with('type::feature').and_return(feature_label_present)
+ allow(fake_helper).to receive(:mr_has_labels?).with('type::bug').and_return(bug_label_present)
+ allow(HTTParty).to receive(:get).with(/page=1/).and_return(version_response)
+ end
+
+ # the stubbed behavior above is the success path
+ it_behaves_like "without a failure"
+
+ context 'with a feature label' do
+ let(:feature_label_present) { true }
+
+ it_behaves_like 'with a failure', described_class::FEATURE_ERROR_MESSAGE
+ end
+
+ context 'without a bug label' do
+ let(:bug_label_present) { false }
+
+ it_behaves_like 'with a failure', described_class::BUG_ERROR_MESSAGE
+ end
+
+ context 'when not an applicable version' do
+ let(:target_branch) { '14-9-stable-ee' }
+
+ it_behaves_like 'with a failure', described_class::VERSION_ERROR_MESSAGE
+ end
+
+ context 'when the version API request fails' do
+ let(:response_success) { false }
+
+ it 'adds a warning' do
+ expect(stable_branch).to receive(:warn).with(described_class::FAILED_VERSION_REQUEST_MESSAGE)
+
+ subject
+ end
+ end
+
+ context 'when more than one page of versions is needed' do
+ # we target a version we know will not be returned in the first request
+ let(:target_branch) { '14-10-stable-ee' }
+
+ let(:first_version_response) do
+ instance_double(
+ HTTParty::Response,
+ success?: response_success,
+ parsed_response: [
+ { 'version' => '15.1.1' },
+ { 'version' => '15.1.0' },
+ { 'version' => '15.0.2' },
+ { 'version' => '15.0.1' }
+ ]
+ )
+ end
+
+ let(:second_version_response) do
+ instance_double(
+ HTTParty::Response,
+ success?: response_success,
+ parsed_response: [
+ { 'version' => '15.0.0' },
+ { 'version' => '14.10.3' },
+ { 'version' => '14.10.2' },
+ { 'version' => '14.9.3' }
+ ]
+ )
+ end
+
+ before do
+ allow(HTTParty).to receive(:get).with(/page=1/).and_return(first_version_response)
+ allow(HTTParty).to receive(:get).with(/page=2/).and_return(second_version_response)
+ end
+
+ it_behaves_like "without a failure"
+ end
+
+ context 'when too many version API requests are made' do
+ let(:parsed_response) { [{ 'version' => '15.0.0' }] }
+
+ it 'adds a warning' do
+ expect(HTTParty).to receive(:get).and_return(version_response).at_least(10).times
+ expect(stable_branch).to receive(:warn).with(described_class::FAILED_VERSION_REQUEST_MESSAGE)
+
+ subject
+ end
+ end
+ end
+ end
+end
diff --git a/spec/tooling/danger/user_types_spec.rb b/spec/tooling/danger/user_types_spec.rb
new file mode 100644
index 00000000000..53556601212
--- /dev/null
+++ b/spec/tooling/danger/user_types_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'gitlab-dangerfiles'
+require 'gitlab/dangerfiles/spec_helper'
+require_relative '../../../tooling/danger/user_types'
+
+RSpec.describe Tooling::Danger::UserTypes, feature_category: :subscription_cost_management do
+ include_context 'with dangerfile'
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+ let(:user_types) { fake_danger.new(helper: fake_helper) }
+
+ describe 'changed files' do
+ subject(:bot_user_types_change_warning) { user_types.bot_user_types_change_warning }
+
+ before do
+ allow(fake_helper).to receive(:modified_files).and_return(modified_files)
+ allow(fake_helper).to receive(:changed_lines).and_return(changed_lines)
+ end
+
+ context 'when has_user_type.rb file is not impacted' do
+ let(:modified_files) { ['app/models/concerns/importable.rb'] }
+ let(:changed_lines) { ['+ANY_CHANGES'] }
+
+ it "doesn't add any warnings" do
+ expect(user_types).not_to receive(:warn)
+
+ bot_user_types_change_warning
+ end
+ end
+
+ context 'when the has_user_type.rb file is impacted' do
+ let(:modified_files) { ['app/models/concerns/has_user_type.rb'] }
+
+ context 'with BOT_USER_TYPES changes' do
+ let(:changed_lines) { ['+BOT_USER_TYPES'] }
+
+ it 'adds warning' do
+ expect(user_types).to receive(:warn).with(described_class::BOT_USER_TYPES_CHANGED_WARNING)
+
+ bot_user_types_change_warning
+ end
+ end
+
+ context 'without BOT_USER_TYPES changes' do
+ let(:changed_lines) { ['+OTHER_CHANGES'] }
+
+ it "doesn't add any warnings" do
+ expect(user_types).not_to receive(:warn)
+
+ bot_user_types_change_warning
+ end
+ end
+ end
+ end
+end
diff --git a/spec/tooling/docs/deprecation_handling_spec.rb b/spec/tooling/docs/deprecation_handling_spec.rb
index 15dd69275c9..94c93d99b94 100644
--- a/spec/tooling/docs/deprecation_handling_spec.rb
+++ b/spec/tooling/docs/deprecation_handling_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Docs::DeprecationHandling do
# Create dummy YAML data based on file name
allow(YAML).to receive(:load_file) do |file_name|
{
- 'name' => file_name[/[a-z]*\.yml/],
+ 'title' => file_name[/[a-z]*\.yml/],
'announcement_milestone' => file_name[/\d+-\d+/].tr('-', '.')
}
end
@@ -29,7 +29,7 @@ RSpec.describe Docs::DeprecationHandling do
entries = arguments[:entries]
expect(milestones).to eq(['14.10', '14.2'])
- expect(entries.map { |e| e['name'] }).to eq(['a.yml', 'b.yml', 'c.yml'])
+ expect(entries.map { |e| e['title'] }).to eq(['a.yml', 'b.yml', 'c.yml'])
end
end
diff --git a/spec/tooling/fixtures/metrics/sample_instrumentation_metric.rb b/spec/tooling/fixtures/metrics/sample_instrumentation_metric.rb
new file mode 100644
index 00000000000..d6b86137b1b
--- /dev/null
+++ b/spec/tooling/fixtures/metrics/sample_instrumentation_metric.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class ActiveUserCountMetric < DatabaseMetric
+ operation :count
+
+ relation { SuperUser.active }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb
index 6084dc194da..3f46b3e79f4 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 do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
- .to eq("spec/{bin,channels,config,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,components}{,/**/}*_spec.rb")
+ .to eq("spec/{bin,channels,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,components}{,/**/}*_spec.rb")
end
end
@@ -121,7 +121,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
- .to eq(%r{spec/(bin|channels|config|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|components)/})
+ .to eq(%r{spec/(bin|channels|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|components)/})
end
end
diff --git a/spec/uploaders/ci/secure_file_uploader_spec.rb b/spec/uploaders/ci/secure_file_uploader_spec.rb
index 4bac591704b..ec7bbf637a1 100644
--- a/spec/uploaders/ci/secure_file_uploader_spec.rb
+++ b/spec/uploaders/ci/secure_file_uploader_spec.rb
@@ -46,12 +46,6 @@ RSpec.describe Ci::SecureFileUploader do
end
end
- describe '.background_upload_enabled?' do
- it 'returns false' do
- expect(described_class.background_upload_enabled?).to eq(false)
- end
- end
-
describe '.default_store' do
context 'when object storage is enabled' do
it 'returns REMOTE' do
diff --git a/spec/uploaders/external_diff_uploader_spec.rb b/spec/uploaders/external_diff_uploader_spec.rb
index ee23c1e36b7..a889181b72c 100644
--- a/spec/uploaders/external_diff_uploader_spec.rb
+++ b/spec/uploaders/external_diff_uploader_spec.rb
@@ -24,29 +24,6 @@ RSpec.describe ExternalDiffUploader do
store_dir: %r[merge_request_diffs/mr-\d+]
end
- describe 'migration to object storage' do
- context 'with object storage disabled' do
- it "is skipped" do
- expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
-
- diff
- end
- end
-
- context 'with object storage enabled' do
- before do
- stub_external_diffs_setting(enabled: true)
- stub_external_diffs_object_storage(background_upload: true)
- end
-
- it 'is scheduled to run after creation' do
- expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with(described_class.name, 'MergeRequestDiff', :external_diff, kind_of(Numeric))
-
- diff
- end
- end
- end
-
describe 'remote file' do
context 'with object storage enabled' do
before do
@@ -57,8 +34,6 @@ RSpec.describe ExternalDiffUploader do
end
it 'can store file remotely' do
- allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
-
diff
expect(diff.external_diff_store).to eq(described_class::Store::REMOTE)
diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb
index 3b8c6f6f881..13a5a7e0549 100644
--- a/spec/uploaders/file_mover_spec.rb
+++ b/spec/uploaders/file_mover_spec.rb
@@ -57,12 +57,6 @@ RSpec.describe FileMover do
.to change { tmp_upload.reload.attributes.values_at('model_id', 'model_type') }
.from([user.id, 'User']).to([snippet.id, 'Snippet'])
end
-
- it 'schedules a background migration' do
- expect_any_instance_of(PersonalFileUploader).to receive(:schedule_background_upload).once
-
- subject
- end
end
context 'when update_markdown fails' do
diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb
index db70441aaf5..f62ab726631 100644
--- a/spec/uploaders/gitlab_uploader_spec.rb
+++ b/spec/uploaders/gitlab_uploader_spec.rb
@@ -6,7 +6,7 @@ require 'carrierwave/storage/fog'
RSpec.describe GitlabUploader do
let(:uploader_class) { Class.new(described_class) }
- subject { uploader_class.new(double) }
+ subject(:uploader) { uploader_class.new(double) }
describe '#file_storage?' do
context 'when file storage is used' do
@@ -161,6 +161,19 @@ RSpec.describe GitlabUploader do
end
end
+ describe '#multi_read' do
+ let(:file) { fixture_file_upload('spec/fixtures/trace/sample_trace', 'text/plain') }
+ let(:byte_offsets) { [[4, 10], [17, 29]] }
+
+ subject { uploader.multi_read(byte_offsets) }
+
+ before do
+ uploader.store!(file)
+ end
+
+ it { is_expected.to eq(%w[Running gitlab-runner]) }
+ end
+
describe '.version' do
subject { uploader_class.version }
diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb
index d1a3fb243ac..b85892a42b5 100644
--- a/spec/uploaders/lfs_object_uploader_spec.rb
+++ b/spec/uploaders/lfs_object_uploader_spec.rb
@@ -25,28 +25,6 @@ RSpec.describe LfsObjectUploader do
store_dir: %r[\h{2}/\h{2}]
end
- describe 'migration to object storage' do
- context 'with object storage disabled' do
- it "is skipped" do
- expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
-
- lfs_object
- end
- end
-
- context 'with object storage enabled' do
- before do
- stub_lfs_object_storage(background_upload: true)
- end
-
- it 'is scheduled to run after creation' do
- expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with(described_class.name, 'LfsObject', :file, kind_of(Numeric))
-
- lfs_object
- end
- end
- end
-
describe 'remote file' do
let(:lfs_object) { create(:lfs_object, :object_storage, :with_file) }
@@ -56,8 +34,6 @@ RSpec.describe LfsObjectUploader do
end
it 'can store file remotely' do
- allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
-
lfs_object
expect(lfs_object.file_store).to eq(described_class::Store::REMOTE)
diff --git a/spec/uploaders/packages/composer/cache_uploader_spec.rb b/spec/uploaders/packages/composer/cache_uploader_spec.rb
index a4ba4cc2a1e..7ceaa24f463 100644
--- a/spec/uploaders/packages/composer/cache_uploader_spec.rb
+++ b/spec/uploaders/packages/composer/cache_uploader_spec.rb
@@ -33,8 +33,6 @@ RSpec.describe Packages::Composer::CacheUploader do
end
it 'can store file remotely' do
- allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
-
cache_file
expect(cache_file.file_store).to eq(described_class::Store::REMOTE)
diff --git a/spec/uploaders/packages/debian/component_file_uploader_spec.rb b/spec/uploaders/packages/debian/component_file_uploader_spec.rb
index de60ec94acf..bee82fb2715 100644
--- a/spec/uploaders/packages/debian/component_file_uploader_spec.rb
+++ b/spec/uploaders/packages/debian/component_file_uploader_spec.rb
@@ -38,8 +38,6 @@ RSpec.describe Packages::Debian::ComponentFileUploader do
end
it 'can store file remotely' do
- allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
-
component_file
expect(component_file.file_store).to eq(described_class::Store::REMOTE)
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 dbbf69e3c8d..96655edb186 100644
--- a/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb
+++ b/spec/uploaders/packages/debian/distribution_release_file_uploader_spec.rb
@@ -38,8 +38,6 @@ RSpec.describe Packages::Debian::DistributionReleaseFileUploader do
end
it 'can store file remotely' do
- allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
-
distribution
expect(distribution.file_store).to eq(described_class::Store::REMOTE)
diff --git a/spec/uploaders/packages/package_file_uploader_spec.rb b/spec/uploaders/packages/package_file_uploader_spec.rb
index 0c7bf6432cb..7d270ad03c9 100644
--- a/spec/uploaders/packages/package_file_uploader_spec.rb
+++ b/spec/uploaders/packages/package_file_uploader_spec.rb
@@ -33,8 +33,6 @@ RSpec.describe Packages::PackageFileUploader do
end
it 'can store file remotely' do
- allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
-
package_file
expect(package_file.file_store).to eq(described_class::Store::REMOTE)
diff --git a/spec/uploaders/packages/rpm/repository_file_uploader_spec.rb b/spec/uploaders/packages/rpm/repository_file_uploader_spec.rb
index 720e109533b..b3767ae179a 100644
--- a/spec/uploaders/packages/rpm/repository_file_uploader_spec.rb
+++ b/spec/uploaders/packages/rpm/repository_file_uploader_spec.rb
@@ -33,8 +33,6 @@ RSpec.describe Packages::Rpm::RepositoryFileUploader do
end
it 'can store file remotely' do
- allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
-
repository_file
expect(repository_file.file_store).to eq(described_class::Store::REMOTE)
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index d2eae5d7a54..1373ccac23d 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -43,8 +43,8 @@ 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
@@ -52,7 +52,7 @@ RSpec.describe PersonalFileUploader 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+$]
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/\h+/\S+$]
it_behaves_like '#base_dir'
it_behaves_like '#to_h'
@@ -67,7 +67,7 @@ RSpec.describe PersonalFileUploader do
it_behaves_like 'builds correct paths',
store_dir: %r[\d+/\h+],
- upload_path: %r[^personal_snippet\/\d+\/\h+\/<filename>]
+ upload_path: %r[^personal_snippet/\d+/\h+/<filename>]
it_behaves_like '#base_dir'
it_behaves_like '#to_h'
diff --git a/spec/uploaders/terraform/state_uploader_spec.rb b/spec/uploaders/terraform/state_uploader_spec.rb
index bd8e7fbc016..2c17edf4e67 100644
--- a/spec/uploaders/terraform/state_uploader_spec.rb
+++ b/spec/uploaders/terraform/state_uploader_spec.rb
@@ -72,12 +72,6 @@ RSpec.describe Terraform::StateUploader do
end
end
- describe '.background_upload_enabled?' do
- it 'returns false' do
- expect(described_class.background_upload_enabled?).to eq(false)
- end
- end
-
describe '.proxy_download_enabled?' do
it 'returns true' do
expect(described_class.proxy_download_enabled?).to eq(true)
diff --git a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
deleted file mode 100644
index a481939ed7a..00000000000
--- a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ObjectStorage::BackgroundMoveWorker do
- let(:local) { ObjectStorage::Store::LOCAL }
- let(:remote) { ObjectStorage::Store::REMOTE }
-
- def perform
- described_class.perform_async(uploader_class.name, subject_class, file_field, subject_id)
- end
-
- context 'for LFS' do
- let!(:lfs_object) { create(:lfs_object, :with_file, file_store: local) }
- let(:uploader_class) { LfsObjectUploader }
- let(:subject_class) { LfsObject }
- let(:file_field) { :file }
- let(:subject_id) { lfs_object.id }
-
- context 'when object storage is enabled' do
- before do
- stub_lfs_object_storage(background_upload: true)
- end
-
- it 'uploads object to storage', :sidekiq_might_not_need_inline do
- expect { perform }.to change { lfs_object.reload.file_store }.from(local).to(remote)
- end
-
- context 'when background upload is disabled' do
- before do
- allow(Gitlab.config.lfs.object_store).to receive(:background_upload) { false }
- end
-
- it 'is skipped' do
- expect { perform }.not_to change { lfs_object.reload.file_store }
- end
- end
- end
-
- context 'when object storage is disabled' do
- before do
- stub_lfs_object_storage(enabled: false)
- end
-
- it "doesn't migrate files" do
- perform
-
- expect(lfs_object.reload.file_store).to eq(local)
- end
- end
- end
-
- context 'for job artifacts' do
- let(:artifact) { create(:ci_job_artifact, :archive) }
- let(:uploader_class) { JobArtifactUploader }
- let(:subject_class) { Ci::JobArtifact }
- let(:file_field) { :file }
- let(:subject_id) { artifact.id }
-
- context 'when local storage is used' do
- let(:store) { local }
-
- context 'and remote storage is defined' do
- before do
- stub_artifacts_object_storage(background_upload: true)
- end
-
- it "migrates file to remote storage", :sidekiq_might_not_need_inline do
- perform
-
- expect(artifact.reload.file_store).to eq(remote)
- end
- end
- end
- end
-
- context 'for uploads' do
- let!(:project) { create(:project, :with_avatar) }
- let(:uploader_class) { AvatarUploader }
- let(:file_field) { :avatar }
-
- context 'when local storage is used' do
- let(:store) { local }
-
- context 'and remote storage is defined' do
- before do
- stub_uploads_object_storage(uploader_class, background_upload: true)
- end
-
- describe 'supports using the model' do
- let(:subject_class) { project.class }
- let(:subject_id) { project.id }
-
- it "migrates file to remote storage", :sidekiq_might_not_need_inline do
- perform
- project.reload
- BatchLoader::Executor.clear_current
-
- expect(project.avatar).not_to be_file_storage
- end
- end
-
- describe 'supports using the Upload' do
- let(:subject_class) { Upload }
- let(:subject_id) { project.avatar.upload.id }
-
- it "migrates file to remote storage", :sidekiq_might_not_need_inline do
- perform
-
- expect(project.reload.avatar).not_to be_file_storage
- end
- end
- end
- end
- end
-end
diff --git a/spec/validators/iso8601_date_validator_spec.rb b/spec/validators/iso8601_date_validator_spec.rb
new file mode 100644
index 00000000000..8b9b8a5bb8f
--- /dev/null
+++ b/spec/validators/iso8601_date_validator_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Iso8601DateValidator do
+ subject do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'deploy_tokens'
+
+ attribute :expires_at
+
+ validates :expires_at, iso8601_date: true
+
+ def self.name
+ "DeployToken"
+ end
+ end.new
+ end
+
+ it 'passes a valid date' do
+ subject.expires_at = DateTime.now
+
+ expect(subject.valid?).to be_truthy
+ end
+
+ it 'errors on an invalid date' do
+ subject.expires_at = '2-12-2022'
+
+ expect(subject.valid?).to be_falsy
+ expect(subject.errors.full_messages).to include('Expires at must be in ISO 8601 format')
+ end
+end
diff --git a/spec/views/admin/application_settings/_ci_cd.html.haml_spec.rb b/spec/views/admin/application_settings/_ci_cd.html.haml_spec.rb
index 12593b88009..d5aa7139e2b 100644
--- a/spec/views/admin/application_settings/_ci_cd.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/_ci_cd.html.haml_spec.rb
@@ -14,7 +14,8 @@ RSpec.describe 'admin/application_settings/_ci_cd' do
ci_pipeline_schedules: 40,
ci_needs_size_limit: 50,
ci_registered_group_runners: 60,
- ci_registered_project_runners: 70
+ ci_registered_project_runners: 70,
+ pipeline_hierarchy_size: 300
}
end
@@ -58,6 +59,11 @@ RSpec.describe 'admin/application_settings/_ci_cd' do
expect(rendered).to have_field('Maximum number of runners registered per project', type: 'number')
expect(page.find_field('Maximum number of runners registered per project').value).to eq('70')
+
+ expect(rendered).to have_field("Maximum number of downstream pipelines in a pipeline's hierarchy tree",
+type: 'number')
+ expect(page.find_field("Maximum number of downstream pipelines in a pipeline's hierarchy tree").value)
+ .to eq('300')
end
it 'does not display the plan name when there is only one plan' do
diff --git a/spec/views/admin/application_settings/_repository_check.html.haml_spec.rb b/spec/views/admin/application_settings/_repository_check.html.haml_spec.rb
index fbabc890a8b..dc3459f84ef 100644
--- a/spec/views/admin/application_settings/_repository_check.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/_repository_check.html.haml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'admin/application_settings/_repository_check.html.haml' do
+RSpec.describe 'admin/application_settings/_repository_check.html.haml', feature_category: :source_code_management do
let_it_be(:user) { create(:admin) }
let_it_be(:application_setting) { build(:application_setting) }
@@ -40,9 +40,28 @@ RSpec.describe 'admin/application_settings/_repository_check.html.haml' do
render
expect(rendered).to have_field('Enable automatic repository housekeeping')
- expect(rendered).to have_field('Incremental repack period')
- expect(rendered).to have_field('Full repack period')
- expect(rendered).to have_field('Git GC period')
+ expect(rendered).to have_field('Optimize repository period')
+
+ # TODO: Remove it along with optimized_housekeeping feature flag
+ expect(rendered).not_to have_field('Incremental repack period')
+ expect(rendered).not_to have_field('Full repack period')
+ expect(rendered).not_to have_field('Git GC period')
+ end
+
+ context 'when optimized_housekeeping is disabled' do
+ before do
+ stub_feature_flags(optimized_housekeeping: false)
+ end
+
+ it 'renders the correct setting subsection content' do
+ render
+
+ expect(rendered).to have_field('Enable automatic repository housekeeping')
+ expect(rendered).to have_field('Incremental repack period')
+ expect(rendered).to have_field('Full repack period')
+ expect(rendered).to have_field('Git GC period')
+ expect(rendered).not_to have_field('Optimize repository period')
+ end
end
end
diff --git a/spec/views/admin/application_settings/general.html.haml_spec.rb b/spec/views/admin/application_settings/general.html.haml_spec.rb
index a8c7bec36e3..f229fd2dcdc 100644
--- a/spec/views/admin/application_settings/general.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/general.html.haml_spec.rb
@@ -75,18 +75,6 @@ RSpec.describe 'admin/application_settings/general.html.haml' do
expect(rendered).to have_css('#js-jira_connect-settings')
end
-
- context 'when the jira_connect_oauth_self_managed_setting feature flag is disabled' do
- before do
- stub_feature_flags(jira_connect_oauth_self_managed_setting: false)
- end
-
- it 'does not show the jira connect settings section' do
- render
-
- expect(rendered).not_to have_css('#js-jira_connect-settings')
- end
- end
end
describe 'sign-up restrictions' do
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index 6e06af92232..337964f1354 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -7,9 +7,7 @@ RSpec.describe 'admin/dashboard/index.html.haml' do
include StubVersion
before do
- counts = Admin::DashboardController::COUNTED_ITEMS.each_with_object({}) do |item, hash|
- hash[item] = 100
- end
+ counts = Admin::DashboardController::COUNTED_ITEMS.index_with { 100 }
assign(:counts, counts)
assign(:projects, create_list(:project, 1))
diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb
index 1d26afcc567..c041c41a412 100644
--- a/spec/views/help/index.html.haml_spec.rb
+++ b/spec/views/help/index.html.haml_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'help/index' do
context 'when logged in' do
def version_link_regexp(path)
base_url = "#{view.source_host_url}/#{view.source_code_group}"
- %r{#{Regexp.escape(base_url)}/(gitlab|gitlab\-foss)/#{Regexp.escape(path)}}
+ %r{#{Regexp.escape(base_url)}/(gitlab|gitlab-foss)/#{Regexp.escape(path)}}
end
before do
diff --git a/spec/views/import/gitlab_projects/new.html.haml_spec.rb b/spec/views/import/gitlab_projects/new.html.haml_spec.rb
index c09c798f487..68e7019c892 100644
--- a/spec/views/import/gitlab_projects/new.html.haml_spec.rb
+++ b/spec/views/import/gitlab_projects/new.html.haml_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'import/gitlab_projects/new.html.haml' do
render
- expect(rendered).to have_select('namespace_id', count: 1)
+ expect(rendered).to have_css('.js-vue-new-project-url-select', count: 1)
end
end
end
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 d0d220fed66..080a53cc1a2 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -96,10 +96,24 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
describe 'Commits' do
- it 'has a link to the project commits path' do
- render
+ context 'when the use_ref_type_parameter flag is not enabled' do
+ before do
+ stub_feature_flags(use_ref_type_parameter: false)
+ end
- expect(rendered).to have_link('Commits', href: project_commits_path(project, current_ref), id: 'js-onboarding-commits-link')
+ it 'has a link to the project commits path' do
+ render
+
+ expect(rendered).to have_link('Commits', href: project_commits_path(project, current_ref), id: 'js-onboarding-commits-link')
+ end
+ end
+
+ context 'when the use_ref_type_parameter flag is enabled' do
+ it 'has a link to the fully qualified project commits path' do
+ render
+
+ expect(rendered).to have_link('Commits', href: project_commits_path(project, current_ref, ref_type: 'heads'), id: 'js-onboarding-commits-link')
+ end
end
end
@@ -120,10 +134,24 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
describe 'Contributors' do
- it 'has a link to the project contributors path' do
- render
+ context 'and the use_ref_type_parameter flag is disabled' do
+ before do
+ stub_feature_flags(use_ref_type_parameter: false)
+ end
- expect(rendered).to have_link('Contributors', href: project_graph_path(project, current_ref))
+ it 'has a link to the project contributors path' do
+ render
+
+ expect(rendered).to have_link('Contributors', href: project_graph_path(project, current_ref))
+ end
+ end
+
+ context 'and the use_ref_type_parameter flag is enabled' do
+ it 'has a link to the project contributors path' do
+ render
+
+ expect(rendered).to have_link('Contributors', href: project_graph_path(project, current_ref, ref_type: 'heads'))
+ end
end
end
diff --git a/spec/views/profiles/keys/_form.html.haml_spec.rb b/spec/views/profiles/keys/_form.html.haml_spec.rb
index 3c61afb21c5..dd8af14100a 100644
--- a/spec/views/profiles/keys/_form.html.haml_spec.rb
+++ b/spec/views/profiles/keys/_form.html.haml_spec.rb
@@ -32,6 +32,11 @@ RSpec.describe 'profiles/keys/_form.html.haml' do
expect(rendered).to have_text('Key titles are publicly visible.')
end
+ it 'has the usage type field', :aggregate_failures do
+ expect(page).to have_select _('Usage type'),
+ selected: 'Authentication & Signing', options: ['Authentication & Signing', 'Authentication', 'Signing']
+ end
+
it 'has the expires at field', :aggregate_failures do
expect(rendered).to have_field('Expiration date', type: 'text')
expect(page.find_field('Expiration date')['min']).to eq(l(1.day.from_now, format: "%Y-%m-%d"))
diff --git a/spec/views/profiles/keys/_key.html.haml_spec.rb b/spec/views/profiles/keys/_key.html.haml_spec.rb
index 1040541332d..d2e27bd2ee0 100644
--- a/spec/views/profiles/keys/_key.html.haml_spec.rb
+++ b/spec/views/profiles/keys/_key.html.haml_spec.rb
@@ -30,6 +30,26 @@ RSpec.describe 'profiles/keys/_key.html.haml' do
expect(response).to render_template(partial: 'shared/ssh_keys/_key_delete')
end
+ context 'displays the usage type' do
+ where(:usage_type, :usage_type_text) do
+ [
+ [:auth, 'Authentication'],
+ [:auth_and_signing, 'Authentication & Signing'],
+ [:signing, 'Signing']
+ ]
+ end
+
+ with_them do
+ let(:key) { create(:key, user: user, usage_type: usage_type) }
+
+ it 'renders usage type text' do
+ render
+
+ expect(rendered).to have_text(usage_type_text)
+ end
+ end
+ end
+
context 'when the key has not been used' do
let_it_be(:key) do
create(:personal_key,
diff --git a/spec/views/profiles/keys/_key_details.html.haml_spec.rb b/spec/views/profiles/keys/_key_details.html.haml_spec.rb
new file mode 100644
index 00000000000..c223d6702c5
--- /dev/null
+++ b/spec/views/profiles/keys/_key_details.html.haml_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'profiles/keys/_key_details.html.haml' do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ assign(:key, key)
+ allow(view).to receive(:is_admin).and_return(false)
+ end
+
+ describe 'displays the usage type' do
+ where(:usage_type, :usage_type_text) do
+ [
+ [:auth, 'Authentication'],
+ [:auth_and_signing, 'Authentication & Signing'],
+ [:signing, 'Signing']
+ ]
+ end
+
+ with_them do
+ let(:key) { create(:key, user: user, usage_type: usage_type) }
+
+ it 'renders usage type text' do
+ render
+
+ expect(rendered).to have_text(usage_type_text)
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/_files.html.haml_spec.rb b/spec/views/projects/_files.html.haml_spec.rb
new file mode 100644
index 00000000000..b6a8b4735b0
--- /dev/null
+++ b/spec/views/projects/_files.html.haml_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'projects/_files' do
+ include ProjectForksHelper
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:source_project) { create(:project, :repository, :public) }
+
+ context 'when the project is a fork' do
+ let_it_be(:project) { fork_project(source_project, user, { repository: true }) }
+
+ before do
+ assign(:project, project)
+ assign(:ref, project.default_branch)
+ assign(:path, '/')
+ assign(:id, project.commit.id)
+
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
+ context 'when user can read fork source' do
+ before do
+ allow(view).to receive(:can?).with(user, :read_project, source_project).and_return(true)
+ end
+
+ it 'shows the forked-from project' do
+ render
+
+ expect(rendered).to have_content("Forked from #{source_project.full_name}")
+ expect(rendered).to have_content("Up to date with upstream repository")
+ end
+
+ context 'when fork_divergence_counts is disabled' do
+ before do
+ stub_feature_flags(fork_divergence_counts: false)
+ end
+
+ it 'does not show fork info' do
+ render
+
+ expect(rendered).not_to have_content("Forked from #{source_project.full_name}")
+ expect(rendered).not_to have_content("Up to date with upstream repository")
+ end
+ end
+ end
+
+ context 'when user cannot read fork source' do
+ before do
+ allow(view).to receive(:can?).with(user, :read_project, source_project).and_return(false)
+ end
+
+ it 'does not show the forked-from project' do
+ render
+
+ expect(rendered).to have_content("Forked from an inaccessible project")
+ end
+
+ context 'when fork_divergence_counts is disabled' do
+ before do
+ stub_feature_flags(fork_divergence_counts: false)
+ end
+
+ it 'does not show fork info' do
+ render
+
+ expect(rendered).not_to have_content("Forked from an inaccessible project")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/_flash_messages.html.haml_spec.rb b/spec/views/projects/_flash_messages.html.haml_spec.rb
index e1858229208..231aa12d920 100644
--- a/spec/views/projects/_flash_messages.html.haml_spec.rb
+++ b/spec/views/projects/_flash_messages.html.haml_spec.rb
@@ -12,10 +12,10 @@ RSpec.describe 'projects/_flash_messages' do
before do
allow(view).to receive(:current_user).and_return(user)
- allow(view).to receive(:can?).with(user, :download_code, project).and_return(true)
+ allow(view).to receive(:can?).with(user, :read_code, project).and_return(true)
end
- context 'when current_user has download_code permission' do
+ context 'when current_user has read_code permission' do
context 'when user has a terraform state' do
let_it_be(:project) { create(:project) }
let_it_be(:terraform_state) { create(:terraform_state, :locked, :with_version, project: project) }
diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb
index 78131937d3c..6f6a2d9a04d 100644
--- a/spec/views/projects/_home_panel.html.haml_spec.rb
+++ b/spec/views/projects/_home_panel.html.haml_spec.rb
@@ -190,22 +190,50 @@ RSpec.describe 'projects/_home_panel' do
end
context 'user can read fork source' do
- it 'shows the forked-from project' do
+ before do
allow(view).to receive(:can?).with(user, :read_project, source_project).and_return(true)
+ end
+ it 'does not show the forked-from project' do
render
- expect(rendered).to have_content("Forked from #{source_project.full_name}")
+ expect(rendered).not_to have_content("Forked from #{source_project.full_name}")
+ end
+
+ context 'when fork_divergence_counts is disabled' do
+ before do
+ stub_feature_flags(fork_divergence_counts: false)
+ end
+
+ it 'shows the forked-from project' do
+ render
+
+ expect(rendered).to have_content("Forked from #{source_project.full_name}")
+ end
end
end
context 'user cannot read fork source' do
- it 'does not show the forked-from project' do
+ before do
allow(view).to receive(:can?).with(user, :read_project, source_project).and_return(false)
+ end
+ it 'shows the message that forked project is inaccessible' do
render
- expect(rendered).to have_content("Forked from an inaccessible project")
+ expect(rendered).not_to have_content("Forked from an inaccessible project")
+ end
+
+ context 'when fork_divergence_counts is disabled' do
+ before do
+ stub_feature_flags(fork_divergence_counts: false)
+ end
+
+ it 'shows the message that forked project is inaccessible' do
+ render
+
+ expect(rendered).to have_content("Forked from an inaccessible project")
+ end
end
end
end
diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb
index 4d5c987ce37..1d9e5e782e5 100644
--- a/spec/views/projects/commit/show.html.haml_spec.rb
+++ b/spec/views/projects/commit/show.html.haml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'projects/commit/show.html.haml' do
+RSpec.describe 'projects/commit/show.html.haml', feature_category: :source_code do
let(:project) { create(:project, :repository) }
let(:commit) { project.commit }
@@ -76,4 +76,44 @@ RSpec.describe 'projects/commit/show.html.haml' do
end
end
end
+
+ context 'when commit is signed' do
+ let(:page) { Nokogiri::HTML.parse(rendered) }
+ let(:badge) { page.at('.gpg-status-box') }
+ let(:badge_attributes) { badge.attributes }
+ let(:title) { badge_attributes['data-title'].value }
+ let(:content) { badge_attributes['data-content'].value }
+
+ before do
+ render
+ end
+
+ context 'with GPG' do
+ let(:commit) { project.commit(GpgHelpers::SIGNED_COMMIT_SHA) }
+
+ it 'renders unverified badge' do
+ expect(title).to include('This commit was signed with an <strong>unverified</strong> signature.')
+ expect(content).to include(commit.signature.gpg_key_primary_keyid)
+ end
+ end
+
+ context 'with SSH' do
+ let(:commit) { project.commit('7b5160f9bb23a3d58a0accdbe89da13b96b1ece9') }
+
+ it 'renders unverified badge' do
+ expect(title).to include('This commit was signed with an <strong>unverified</strong> signature.')
+ expect(content).to match(/SSH key fingerprint:[\s\S]+Unknown/)
+ end
+ end
+
+ context 'with X.509' do
+ let(:commit) { project.commit('189a6c924013fc3fe40d6f1ec1dc20214183bc97') }
+
+ it 'renders unverified badge' do
+ expect(title).to include('This commit was signed with an <strong>unverified</strong> signature.')
+ expect(content).to include(commit.signature.x509_certificate.subject_key_identifier.tr(":", " "))
+ expect(content).to include(commit.signature.x509_certificate.email)
+ end
+ 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 ba6f7068024..deec2db6865 100644
--- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -9,10 +9,13 @@ RSpec.describe 'projects/issues/_related_branches' do
let(:status) { pipeline.detailed_status(build(:user)) }
before do
- assign(:related_branches, [
- { name: 'other', link: 'link-to-other', pipeline_status: nil },
- { name: 'feature', link: 'link-to-feature', pipeline_status: status }
- ])
+ assign(:related_branches,
+ [
+ { name: 'other', link: 'link-to-other', pipeline_status: nil },
+ { name: 'feature', link: 'link-to-feature', pipeline_status: status }
+
+ ]
+ )
render
end
diff --git a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
index 3776af9e757..7886a811c9a 100644
--- a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
+++ b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'projects/notes/_more_actions_dropdown' do
it 'shows Report abuse to admin button if not editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: false, note: note
- expect(rendered).to have_link('Report abuse to admin')
+ expect(rendered).to have_link('Report abuse to administrator')
end
it 'does not show the More actions button if not editable and current users comment' do
@@ -26,10 +26,10 @@ RSpec.describe 'projects/notes/_more_actions_dropdown' do
expect(rendered).not_to have_selector('.dropdown.more-actions')
end
- it 'shows Report abuse to admin and Delete buttons if editable and not current users comment' do
+ it 'shows Report abuse and Delete buttons if editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: true, note: note
- expect(rendered).to have_link('Report abuse to admin')
+ expect(rendered).to have_link('Report abuse to administrator')
expect(rendered).to have_link('Delete comment')
end
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
index 7e300fb1e6e..b9c7da20d1a 100644
--- a/spec/views/projects/pipelines/show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'projects/pipelines/show' do
+RSpec.describe 'projects/pipelines/show', feature_category: :pipeline_authoring do
include Devise::Test::ControllerHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
@@ -13,7 +13,6 @@ RSpec.describe 'projects/pipelines/show' do
before do
assign(:project, project)
assign(:pipeline, presented_pipeline)
- stub_feature_flags(pipeline_tabs_vue: false)
end
context 'when pipeline has errors' do
@@ -24,14 +23,14 @@ RSpec.describe 'projects/pipelines/show' do
it 'shows errors' do
render
- expect(rendered).to have_content('Found errors in your .gitlab-ci.yml')
+ expect(rendered).to have_content('Unable to create pipeline')
expect(rendered).to have_content('some errors')
end
it 'does not render the pipeline tabs' do
render
- expect(rendered).not_to have_css('ul.pipelines-tabs')
+ expect(rendered).not_to have_selector('#js-pipeline-tabs')
end
end
@@ -39,13 +38,13 @@ RSpec.describe 'projects/pipelines/show' do
it 'does not show errors' do
render
- expect(rendered).not_to have_content('Found errors in your .gitlab-ci.yml')
+ expect(rendered).not_to have_content('Unable to create pipeline')
end
it 'renders the pipeline tabs' do
render
- expect(rendered).to have_css('ul.pipelines-tabs')
+ expect(rendered).to have_selector('#js-pipeline-tabs')
end
end
end
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 62a52bcf83f..5a1ae715f8f 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe 'projects/tree/show' do
it 'displays correctly' do
render
- expect(rendered).to have_css('.js-project-refs-dropdown .dropdown-toggle-text', text: ref)
+ expect(rendered).to have_css('#js-tree-ref-switcher')
end
end
end
diff --git a/spec/views/search/_results.html.haml_spec.rb b/spec/views/search/_results.html.haml_spec.rb
index 2149c394320..e81462ee518 100644
--- a/spec/views/search/_results.html.haml_spec.rb
+++ b/spec/views/search/_results.html.haml_spec.rb
@@ -3,36 +3,60 @@
require 'spec_helper'
RSpec.describe 'search/_results' do
- let(:user) { create(:user) }
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+
let(:search_objects) { Issue.page(1).per(2) }
let(:scope) { 'issues' }
let(:term) { 'foo' }
+ let(:search_results) { instance_double('Gitlab::SearchResults', { formatted_count: 10, current_user: user } ) }
+ let(:search_service) { class_double(SearchServicePresenter, scope: scope, search: term, current_user: user) }
before do
controller.params[:action] = 'show'
controller.params[:search] = term
- allow(self).to receive(:current_user).and_return(user)
- allow(@search_results).to receive(:formatted_count).with(scope).and_return(10)
- allow(self).to receive(:search_count_path).with(any_args).and_return("test count link")
- allow(self).to receive(:search_path).with(any_args).and_return("link test")
-
- stub_feature_flags(search_page_vertical_nav: false)
-
create_list(:issue, 3)
- @search_objects = search_objects
- @scope = scope
- @search_term = term
- @search_service = SearchServicePresenter.new(SearchService.new(user, search: term, scope: scope))
+ allow(view).to receive(:current_user) { user }
+ assign(:search_count_path, 'test count link')
+ assign(:search_path, 'link test')
+ assign(:search_results, search_results)
+ assign(:search_objects, search_objects)
+ assign(:search_term, term)
+ assign(:scope, scope)
+ @search_service = SearchServicePresenter.new(SearchService.new(user, search: term, scope: scope))
allow(@search_service).to receive(:search_objects).and_return(search_objects)
end
- it 'displays the page size' do
- render
+ where(search_page_vertical_nav_enabled: [true, false])
+
+ with_them do
+ describe 'page size' do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ end
+
+ context 'when search results have a count' do
+ it 'displays the page size' do
+ render
+
+ expect(rendered).to have_content('Showing 1 - 2 of 3 issues for foo')
+ end
+ end
+
+ context 'when search results do not have a count' do
+ let(:search_objects) { Issue.page(1).per(2).without_count }
+
+ it 'does not display the page size' do
+ render
- expect(rendered).to have_content('Showing 1 - 2 of 3 issues for foo')
+ expect(rendered).not_to have_content(/Showing .* of .*/)
+ end
+ end
+ end
end
context 'when searching notes which contain quotes in markdown' do
@@ -51,18 +75,6 @@ RSpec.describe 'search/_results' do
end
end
- context 'when search results do not have a count' do
- before do
- @search_objects = @search_objects.without_count
- end
-
- it 'does not display the page size' do
- render
-
- expect(rendered).not_to have_content(/Showing .* of .*/)
- end
- end
-
context 'rendering all types of search results' do
let_it_be(:project) { create(:project, :repository, :wiki_repo) }
let_it_be(:issue) { create(:issue, project: project, title: 'testing') }
diff --git a/spec/views/search/show.html.haml_spec.rb b/spec/views/search/show.html.haml_spec.rb
index 5f9c6c65a08..26ec2c6ae74 100644
--- a/spec/views/search/show.html.haml_spec.rb
+++ b/spec/views/search/show.html.haml_spec.rb
@@ -2,27 +2,32 @@
require 'spec_helper'
-RSpec.describe 'search/show' do
+RSpec.describe 'search/show', feature_category: :global_search do
let(:search_term) { nil }
let(:user) { build(:user) }
+ let(:search_service_presenter) do
+ instance_double(SearchServicePresenter, without_count?: false, advanced_search_enabled?: false)
+ end
before do
stub_template "search/_category.html.haml" => 'Category Partial'
stub_template "search/_results.html.haml" => 'Results Partial'
+
+ assign(:search_service, search_service_presenter)
end
context 'search_page_vertical_nav feature flag enabled' do
before do
allow(view).to receive(:current_user) { user }
assign(:search_term, search_term)
-
- render
end
context 'when search term is supplied' do
let(:search_term) { 'Search Foo' }
it 'will not render category partial' do
+ render
+
expect(rendered).not_to render_template('search/_category')
expect(rendered).to render_template('search/_results')
end
@@ -34,17 +39,19 @@ RSpec.describe 'search/show' do
stub_feature_flags(search_page_vertical_nav: false)
assign(:search_term, search_term)
-
- render
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 partials' do
+ render
+
expect(rendered).not_to render_template('search/_category')
expect(rendered).not_to render_template('search/_results')
end
@@ -54,6 +61,8 @@ RSpec.describe 'search/show' do
let(:search_term) { 'Search Foo' }
it 'renders partials' do
+ render
+
expect(rendered).to render_template('search/_category')
expect(rendered).to render_template('search/_results')
end
@@ -73,8 +82,8 @@ RSpec.describe 'search/show' do
end
context 'search with full count' do
- before do
- assign(:without_count, false)
+ let(:search_service_presenter) do
+ instance_double(SearchServicePresenter, without_count?: false, advanced_search_enabled?: false)
end
it 'renders meta tags for a group' do
@@ -96,8 +105,8 @@ RSpec.describe 'search/show' do
end
context 'search without full count' do
- before do
- assign(:without_count, true)
+ let(:search_service_presenter) do
+ instance_double(SearchServicePresenter, without_count?: true, advanced_search_enabled?: false)
end
it 'renders meta tags for a group' do
diff --git a/spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb b/spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb
new file mode 100644
index 00000000000..4387a3f5b07
--- /dev/null
+++ b/spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'shared/gitlab_version/_security_patch_upgrade_alert' do
+ describe 'when show_security_patch_upgrade_alert? is true' do
+ before do
+ allow(view).to receive(:show_security_patch_upgrade_alert?).and_return(true)
+ render
+ end
+
+ it 'renders the security patch upgrade alert' do
+ expect(rendered).to have_selector('#js-security-patch-upgrade-alert')
+ end
+
+ it 'renders the security patch upgrade alert modal' do
+ expect(rendered).to have_selector('#js-security-patch-upgrade-alert-modal')
+ end
+ end
+end
diff --git a/spec/views/shared/ssh_keys/_key_details.html.haml_spec.rb b/spec/views/shared/ssh_keys/_key_delete.html.haml_spec.rb
index 1bee9f7463f..c9bdcabb4b6 100644
--- a/spec/views/shared/ssh_keys/_key_details.html.haml_spec.rb
+++ b/spec/views/shared/ssh_keys/_key_delete.html.haml_spec.rb
@@ -2,19 +2,21 @@
require 'spec_helper'
RSpec.describe 'shared/ssh_keys/_key_delete.html.haml' do
- context 'when the text parameter is used' do
+ context 'when the icon parameter is used' do
it 'has text' do
- render partial: 'shared/ssh_keys/key_delete', formats: :html, locals: { text: 'Button', html_class: '', button_data: '' }
+ render partial: 'shared/ssh_keys/key_delete', formats: :html, locals: { icon: true, button_data: '' }
- expect(rendered).to have_button('Button')
+ expect(rendered).not_to have_button('Delete')
+ expect(rendered).to have_selector('[data-testid=remove-icon]')
end
end
- context 'when the text parameter is not used' do
+ context 'when the icon parameter is not used' do
it 'does not have text' do
- render partial: 'shared/ssh_keys/key_delete', formats: :html, locals: { html_class: '', button_data: '' }
+ render partial: 'shared/ssh_keys/key_delete', formats: :html, locals: { button_data: '' }
expect(rendered).to have_button('Delete')
+ expect(rendered).not_to have_selector('[data-testid=remove-icon]')
end
end
end
diff --git a/spec/workers/bulk_import_worker_spec.rb b/spec/workers/bulk_import_worker_spec.rb
index 0d0b81d2ec0..61c33f123fa 100644
--- a/spec/workers/bulk_import_worker_spec.rb
+++ b/spec/workers/bulk_import_worker_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe BulkImportWorker do
entity_2 = create(:bulk_import_entity, :created, bulk_import: bulk_import)
expect { subject.perform(bulk_import.id) }
- .to change(BulkImports::Tracker, :count)
+ .to change { BulkImports::Tracker.count }
.by(BulkImports::Groups::Stage.new(entity_1).pipelines.size * 2)
expect(entity_1.trackers).not_to be_empty
diff --git a/spec/workers/bulk_imports/entity_worker_spec.rb b/spec/workers/bulk_imports/entity_worker_spec.rb
index e3f0ee65205..4cd37c93d5f 100644
--- a/spec/workers/bulk_imports/entity_worker_spec.rb
+++ b/spec/workers/bulk_imports/entity_worker_spec.rb
@@ -114,6 +114,8 @@ RSpec.describe BulkImports::EntityWorker do
)
subject
+
+ expect(entity.reload.failed?).to eq(true)
end
context 'in first stage' do
diff --git a/spec/workers/bulk_imports/export_request_worker_spec.rb b/spec/workers/bulk_imports/export_request_worker_spec.rb
index 7eb8150fb2e..7260e0c0f67 100644
--- a/spec/workers/bulk_imports/export_request_worker_spec.rb
+++ b/spec/workers/bulk_imports/export_request_worker_spec.rb
@@ -2,9 +2,10 @@
require 'spec_helper'
-RSpec.describe BulkImports::ExportRequestWorker do
+RSpec.describe BulkImports::ExportRequestWorker, 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(:version_url) { 'https://gitlab.example/api/v4/version' }
let(:response_double) { double(code: 200, success?: true, parsed_response: {}) }
@@ -31,73 +32,6 @@ RSpec.describe BulkImports::ExportRequestWorker do
perform_multiple(job_args)
end
- context 'when network error is raised' do
- let(:exception) { BulkImports::NetworkError.new('Export error') }
-
- before do
- allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
- allow(client).to receive(:post).and_raise(exception).twice
- end
- end
-
- context 'when error is retriable' do
- it 'logs retry request and reenqueues' do
- allow(exception).to receive(:retriable?).twice.and_return(true)
-
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger).to receive(:error).with(
- a_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,
- 'exception.backtrace' => anything,
- 'exception.class' => 'BulkImports::NetworkError',
- 'exception.message' => 'Export error',
- 'message' => 'Retrying export request',
- 'importer' => 'gitlab_migration',
- 'source_version' => entity.bulk_import.source_version_info.to_s
- )
- ).twice
- end
-
- expect(described_class).to receive(:perform_in).twice.with(2.seconds, entity.id)
-
- perform_multiple(job_args)
- end
- end
-
- context 'when error is not retriable' do
- it 'logs export failure and marks entity as failed' do
- allow(exception).to receive(:retriable?).twice.and_return(false)
-
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger).to receive(:error).with(
- a_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,
- 'exception.backtrace' => anything,
- 'exception.class' => 'BulkImports::NetworkError',
- 'exception.message' => 'Export error',
- 'message' => "Request to export #{entity.source_type} failed",
- 'importer' => 'gitlab_migration',
- 'source_version' => entity.bulk_import.source_version_info.to_s
- )
- ).twice
- end
-
- perform_multiple(job_args)
-
- failure = entity.failures.last
-
- expect(failure.pipeline_class).to eq('ExportRequestWorker')
- expect(failure.exception_message).to eq('Export error')
- end
- end
- end
-
context 'when source id is nil' do
let(:entity_source_id) { 'gid://gitlab/Model/1234567' }
@@ -179,4 +113,24 @@ RSpec.describe BulkImports::ExportRequestWorker do
it_behaves_like 'requests relations export for api resource'
end
end
+
+ describe '#sidekiq_retries_exhausted' do
+ it 'logs export failure and marks entity as failed' do
+ entity = create(:bulk_import_entity, bulk_import: bulk_import)
+ error = 'Exhausted error!'
+
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger)
+ .to receive(:error)
+ .with(hash_including('message' => "Request to export #{entity.source_type} failed"))
+ end
+
+ described_class
+ .sidekiq_retries_exhausted_block
+ .call({ 'args' => [entity.id] }, StandardError.new(error))
+
+ expect(entity.reload.failed?).to eq(true)
+ expect(entity.failures.last.exception_message).to eq(error)
+ end
+ end
end
diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb
index 23fbc5688ec..03ec6267ca8 100644
--- a/spec/workers/bulk_imports/pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::PipelineWorker do
+RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
let(:pipeline_class) do
Class.new do
def initialize(_); end
@@ -19,6 +19,15 @@ RSpec.describe BulkImports::PipelineWorker do
let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) }
let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
+ let(:pipeline_tracker) do
+ create(
+ :bulk_import_tracker,
+ entity: entity,
+ pipeline_name: 'FakePipeline',
+ status_event: 'enqueue'
+ )
+ end
+
before do
stub_const('FakePipeline', pipeline_class)
@@ -60,45 +69,12 @@ RSpec.describe BulkImports::PipelineWorker do
end
end
- it_behaves_like 'successfully runs the pipeline' do
- let(:pipeline_tracker) do
- create(
- :bulk_import_tracker,
- entity: entity,
- pipeline_name: 'FakePipeline',
- status_event: 'enqueue'
- )
- end
- end
+ it_behaves_like 'successfully runs the pipeline'
- context 'when the pipeline cannot be found' do
- it 'logs the error' do
- pipeline_tracker = create(
- :bulk_import_tracker,
- :finished,
- entity: entity,
- pipeline_name: 'FakePipeline'
- )
-
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
- expect(logger)
- .to receive(:error)
- .with(
- hash_including(
- 'pipeline_tracker_id' => pipeline_tracker.id,
- '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,
- 'message' => 'Unstarted pipeline not found'
- )
- )
- end
-
- expect(BulkImports::EntityWorker)
- .to receive(:perform_async)
- .with(entity.id, pipeline_tracker.stage)
+ context 'when exclusive lease cannot be obtained' do
+ it 'does not run the pipeline' do
+ expect(subject).to receive(:try_obtain_lease).and_return(false)
+ expect(subject).not_to receive(:run)
subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
end
@@ -145,13 +121,15 @@ RSpec.describe BulkImports::PipelineWorker do
.to receive(:track_exception)
.with(
instance_of(StandardError),
- 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,
- pipeline_name: pipeline_tracker.pipeline_name,
- importer: 'gitlab_migration',
- source_version: entity.bulk_import.source_version_info.to_s
+ 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,
+ 'pipeline_name' => pipeline_tracker.pipeline_name,
+ 'importer' => 'gitlab_migration',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
+ )
)
expect(BulkImports::EntityWorker)
@@ -179,6 +157,111 @@ RSpec.describe BulkImports::PipelineWorker do
expect(pipeline_tracker.jid).to eq('jid')
end
+ shared_examples 'successfully runs the pipeline' do
+ it 'runs the given pipeline successfully' do
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger)
+ .to receive(:info)
+ .with(
+ hash_including(
+ 'pipeline_name' => 'FakePipeline',
+ 'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_id' => entity.id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path
+ )
+ )
+ end
+
+ expect(BulkImports::EntityWorker)
+ .to receive(:perform_async)
+ .with(entity.id, pipeline_tracker.stage)
+
+ allow(subject).to receive(:jid).and_return('jid')
+
+ subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+
+ pipeline_tracker.reload
+
+ expect(pipeline_tracker.status_name).to eq(:finished)
+ 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
+
+ expect(BulkImports::EntityWorker)
+ .to receive(:perform_async)
+ .with(entity.id, pipeline_tracker.stage)
+
+ subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ end
+ end
+
+ context 'when pipeline is finished' do
+ let(:pipeline_tracker) do
+ create(
+ :bulk_import_tracker,
+ :finished,
+ entity: entity,
+ pipeline_name: 'FakePipeline'
+ )
+ end
+
+ include_examples 'logs the error'
+ end
+
+ context 'when pipeline is skipped' do
+ let(:pipeline_tracker) do
+ create(
+ :bulk_import_tracker,
+ :skipped,
+ entity: entity,
+ pipeline_name: 'FakePipeline'
+ )
+ end
+
+ include_examples 'logs the error'
+ 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'
+ )
+
+ subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+
+ expect(pipeline_tracker.reload.failed?).to eq(true)
+ end
+ end
+ end
+
context 'when entity is failed' do
it 'marks tracker as skipped and logs the skip' do
pipeline_tracker = create(
@@ -343,23 +426,64 @@ RSpec.describe BulkImports::PipelineWorker do
end
context 'when export status is empty' do
- it 'reenqueues pipeline worker' do
+ before do
allow_next_instance_of(BulkImports::ExportStatus) do |status|
allow(status).to receive(:started?).and_return(false)
allow(status).to receive(:empty?).and_return(true)
allow(status).to receive(:failed?).and_return(false)
end
- expect(described_class)
- .to receive(:perform_in)
- .with(
- described_class::FILE_EXTRACTION_PIPELINE_PERFORM_DELAY,
- pipeline_tracker.id,
- pipeline_tracker.stage,
- entity.id
- )
+ entity.update!(created_at: entity_created_at)
+ end
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ context 'when timeout is not reached' do
+ let(:entity_created_at) { 1.minute.ago }
+
+ it 'reenqueues pipeline worker' do
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(
+ described_class::FILE_EXTRACTION_PIPELINE_PERFORM_DELAY,
+ pipeline_tracker.id,
+ pipeline_tracker.stage,
+ entity.id
+ )
+
+ subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+
+ expect(pipeline_tracker.reload.status_name).to eq(:enqueued)
+ end
+ end
+
+ context 'when timeout is reached' do
+ let(:entity_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)
+ end
end
end
diff --git a/spec/workers/ci/create_downstream_pipeline_worker_spec.rb b/spec/workers/ci/create_downstream_pipeline_worker_spec.rb
index 7a75da850d9..b4add681e67 100644
--- a/spec/workers/ci/create_downstream_pipeline_worker_spec.rb
+++ b/spec/workers/ci/create_downstream_pipeline_worker_spec.rb
@@ -9,19 +9,52 @@ RSpec.describe Ci::CreateDownstreamPipelineWorker do
let(:bridge) { create(:ci_bridge, user: user, pipeline: pipeline) }
- let(:service) { double('pipeline creation service') }
-
describe '#perform' do
context 'when bridge exists' do
- it 'calls cross project pipeline creation service' do
+ let(:service) { double('pipeline creation service') }
+
+ let(:service_result) { ServiceResponse.success(payload: instance_double(Ci::Pipeline, id: 100)) }
+
+ it 'calls cross project pipeline creation service and logs the new pipeline id' do
expect(Ci::CreateDownstreamPipelineService)
.to receive(:new)
.with(project, user)
.and_return(service)
- expect(service).to receive(:execute).with(bridge)
+ expect(service)
+ .to receive(:execute)
+ .with(bridge)
+ .and_return(service_result)
+
+ worker = described_class.new
+ worker.perform(bridge.id)
+
+ expect(worker.logging_extras).to eq({ "extra.ci_create_downstream_pipeline_worker.new_pipeline_id" => 100 })
+ end
+
+ context 'when downstream pipeline creation errors' do
+ let(:service_result) { ServiceResponse.error(message: 'Already has a downstream pipeline') }
+
+ it 'calls cross project pipeline creation service and logs the error' do
+ expect(Ci::CreateDownstreamPipelineService)
+ .to receive(:new)
+ .with(project, user)
+ .and_return(service)
+
+ expect(service)
+ .to receive(:execute)
+ .with(bridge)
+ .and_return(service_result)
+
+ worker = described_class.new
+ worker.perform(bridge.id)
- described_class.new.perform(bridge.id)
+ expect(worker.logging_extras).to eq(
+ {
+ "extra.ci_create_downstream_pipeline_worker.create_error_message" => "Already has a downstream pipeline"
+ }
+ )
+ end
end
end
diff --git a/spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb b/spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb
index fe4bc2421a4..f14b7f9d1d0 100644
--- a/spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb
+++ b/spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb
@@ -46,30 +46,11 @@ RSpec.describe Ci::RefDeleteUnlockArtifactsWorker do
context 'when a locked pipeline with persisted artifacts exists' do
let!(:pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: 'master', project: project, locked: :artifacts_locked) }
- context 'with ci_update_unlocked_job_artifacts disabled' do
- before do
- stub_feature_flags(ci_update_unlocked_job_artifacts: false)
- end
+ it 'logs the correct extra metadata' do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:unlocked_pipelines, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:unlocked_job_artifacts, 2)
- it 'logs the correct extra metadata' do
- expect(worker).to receive(:log_extra_metadata_on_done).with(:unlocked_pipelines, 1)
- expect(worker).to receive(:log_extra_metadata_on_done).with(:unlocked_job_artifacts, 0)
-
- perform
- end
- end
-
- context 'with ci_update_unlocked_job_artifacts enabled' do
- before do
- stub_feature_flags(ci_update_unlocked_job_artifacts: true)
- end
-
- it 'logs the correct extra metadata' do
- expect(worker).to receive(:log_extra_metadata_on_done).with(:unlocked_pipelines, 1)
- expect(worker).to receive(:log_extra_metadata_on_done).with(:unlocked_job_artifacts, 2)
-
- perform
- end
+ perform
end
end
end
diff --git a/spec/workers/ci/runners/process_runner_version_update_worker_spec.rb b/spec/workers/ci/runners/process_runner_version_update_worker_spec.rb
index ff67266c3e8..30b451f2112 100644
--- a/spec/workers/ci/runners/process_runner_version_update_worker_spec.rb
+++ b/spec/workers/ci/runners/process_runner_version_update_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Runners::ProcessRunnerVersionUpdateWorker do
+RSpec.describe Ci::Runners::ProcessRunnerVersionUpdateWorker, feature_category: :runner_fleet do
subject(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/ci/runners/reconcile_existing_runner_versions_cron_worker_spec.rb b/spec/workers/ci/runners/reconcile_existing_runner_versions_cron_worker_spec.rb
index 1292df62ce5..34b1cb33e6b 100644
--- a/spec/workers/ci/runners/reconcile_existing_runner_versions_cron_worker_spec.rb
+++ b/spec/workers/ci/runners/reconcile_existing_runner_versions_cron_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Runners::ReconcileExistingRunnerVersionsCronWorker do
+RSpec.describe Ci::Runners::ReconcileExistingRunnerVersionsCronWorker, feature_category: :runner_fleet do
subject(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
index ece0c5053cb..02190201986 100644
--- a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
@@ -194,6 +194,43 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures do
.to raise_error(NoMethodError, /^undefined method `github_identifiers/)
end
end
+
+ context 'when the record is invalid' do
+ it 'logs an error' do
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:info)
+ .with(
+ {
+ github_identifiers: github_identifiers,
+ message: 'starting importer',
+ project_id: project.id,
+ importer: 'klass_name'
+ }
+ )
+
+ expect(importer_class)
+ .to receive(:new)
+ .with(instance_of(MockRepresantation), project, client)
+ .and_return(importer_instance)
+
+ exception = ActiveRecord::RecordInvalid.new
+ expect(importer_instance)
+ .to receive(:execute)
+ .and_raise(exception)
+
+ expect(Gitlab::Import::ImportFailureService)
+ .to receive(:track)
+ .with(
+ project_id: project.id,
+ exception: exception,
+ error_source: 'klass_name',
+ fail_import: false
+ )
+ .and_call_original
+
+ worker.import(project, client, { 'number' => 10, 'github_id' => 1 })
+ end
+ end
end
describe '#increment_object_counter?' do
diff --git a/spec/workers/concerns/waitable_worker_spec.rb b/spec/workers/concerns/waitable_worker_spec.rb
index bf156c3b8cb..1449c327052 100644
--- a/spec/workers/concerns/waitable_worker_spec.rb
+++ b/spec/workers/concerns/waitable_worker_spec.rb
@@ -14,12 +14,6 @@ RSpec.describe WaitableWorker do
include ApplicationWorker
prepend WaitableWorker
- # This is a workaround for a Ruby 2.3.7 bug. rspec-mocks cannot restore
- # the visibility of prepended modules. See
- # https://github.com/rspec/rspec-mocks/issues/1231 for more details.
- def self.bulk_perform_inline(args_list)
- end
-
def perform(count = 0)
self.class.counter += count
end
@@ -37,27 +31,6 @@ RSpec.describe WaitableWorker do
worker.bulk_perform_and_wait(arguments)
end
-
- context 'when the feature flag `always_async_project_authorizations_refresh` is turned off' do
- before do
- stub_feature_flags(always_async_project_authorizations_refresh: false)
- end
-
- it 'inlines the job' do
- args_list = [[1]]
- expect(worker).to receive(:bulk_perform_inline).with(args_list).and_call_original
- expect(Gitlab::AppJsonLogger).to(
- receive(:info).with(a_hash_including('message' => 'running inline',
- 'class' => 'Gitlab::Foo::Bar::DummyWorker',
- 'job_status' => 'running',
- 'queue' => 'foo_bar_dummy'))
- .once)
-
- worker.bulk_perform_and_wait(args_list)
-
- expect(worker.counter).to eq(1)
- end
- end
end
context 'between 2 and 3 jobs' do
@@ -81,22 +54,6 @@ RSpec.describe WaitableWorker do
end
end
- describe '.bulk_perform_inline' do
- it 'runs the jobs inline' do
- expect(worker).not_to receive(:bulk_perform_async)
-
- worker.bulk_perform_inline([[1], [2]])
-
- expect(worker.counter).to eq(3)
- end
-
- it 'enqueues jobs if an error is raised' do
- expect(worker).to receive(:bulk_perform_async).with([['foo']])
-
- worker.bulk_perform_inline([[1], ['foo']])
- end
- end
-
describe '#perform' do
shared_examples 'perform' do
it 'notifies the JobWaiter when done if the key is provided' do
diff --git a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
index 3cb83a7a5d7..8eda943f36e 100644
--- a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
+++ b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
@@ -398,8 +398,7 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
end
def cleanup_service_response(
- status: :finished,
- repository:,
+ repository:, status: :finished,
cleanup_tags_service_original_size: 100,
cleanup_tags_service_before_truncate_size: 80,
cleanup_tags_service_after_truncate_size: 80,
diff --git a/spec/workers/container_registry/cleanup_worker_spec.rb b/spec/workers/container_registry/cleanup_worker_spec.rb
index ffcb421ce1e..a510b660412 100644
--- a/spec/workers/container_registry/cleanup_worker_spec.rb
+++ b/spec/workers/container_registry/cleanup_worker_spec.rb
@@ -63,19 +63,5 @@ RSpec.describe ContainerRegistry::CleanupWorker, :aggregate_failures do
perform
end
end
-
- context 'with container_registry_delete_repository_with_cron_worker disabled' do
- before do
- stub_feature_flags(container_registry_delete_repository_with_cron_worker: false)
- end
-
- it 'does not run' do
- expect(worker).not_to receive(:reset_stale_deletes)
- expect(worker).not_to receive(:enqueue_delete_container_repository_jobs)
- expect(worker).not_to receive(:log_counts)
-
- subject
- end
- end
end
end
diff --git a/spec/workers/container_registry/migration/guard_worker_spec.rb b/spec/workers/container_registry/migration/guard_worker_spec.rb
index d2bcfef2f5b..4ad2d5c300c 100644
--- a/spec/workers/container_registry/migration/guard_worker_spec.rb
+++ b/spec/workers/container_registry/migration/guard_worker_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
expect(ContainerRegistry::Migration).to receive(timeout).and_call_original
expect { subject }
- .to change(import_aborted_migrations, :count).by(1)
+ .to change { import_aborted_migrations.count }.by(1)
.and change { stale_migration.reload.migration_state }.to('import_aborted')
.and not_change { ongoing_migration.migration_state }
end
@@ -67,7 +67,7 @@ RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
expect(ContainerRegistry::Migration).to receive(timeout).and_call_original
expect { subject }
- .to change(import_skipped_migrations, :count)
+ .to change { import_skipped_migrations.count }
expect(stale_migration.reload.migration_state).to eq('import_skipped')
expect(stale_migration.reload.migration_skipped_reason).to eq('migration_canceled')
@@ -124,11 +124,11 @@ RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
expect(worker).to receive(:log_extra_metadata_on_done).with(:aborted_stale_migrations_count, 1)
expect { subject }
- .to change(pre_importing_migrations, :count).by(-1)
+ .to change { pre_importing_migrations.count }.by(-1)
.and not_change(pre_import_done_migrations, :count)
.and not_change(importing_migrations, :count)
.and not_change(import_done_migrations, :count)
- .and change(import_aborted_migrations, :count).by(1)
+ .and change { import_aborted_migrations.count }.by(1)
.and change { stale_migration.reload.migration_state }.from('pre_importing').to('import_aborted')
.and not_change { ongoing_migration.migration_state }
end
@@ -223,10 +223,10 @@ RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
expect { subject }
.to not_change(pre_importing_migrations, :count)
- .and change(pre_import_done_migrations, :count).by(-1)
+ .and change { pre_import_done_migrations.count }.by(-1)
.and not_change(importing_migrations, :count)
.and not_change(import_done_migrations, :count)
- .and change(import_aborted_migrations, :count).by(1)
+ .and change { import_aborted_migrations.count }.by(1)
.and change { stale_migration.reload.migration_state }.from('pre_import_done').to('import_aborted')
.and not_change { ongoing_migration.migration_state }
end
@@ -252,9 +252,9 @@ RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
expect { subject }
.to not_change(pre_importing_migrations, :count)
.and not_change(pre_import_done_migrations, :count)
- .and change(importing_migrations, :count).by(-1)
+ .and change { importing_migrations.count }.by(-1)
.and not_change(import_done_migrations, :count)
- .and change(import_aborted_migrations, :count).by(1)
+ .and change { import_aborted_migrations.count }.by(1)
.and change { stale_migration.reload.migration_state }.from('importing').to('import_aborted')
.and not_change { ongoing_migration.migration_state }
end
diff --git a/spec/workers/database/batched_background_migration/ci_execution_worker_spec.rb b/spec/workers/database/batched_background_migration/ci_execution_worker_spec.rb
new file mode 100644
index 00000000000..ec77a15c7ef
--- /dev/null
+++ b/spec/workers/database/batched_background_migration/ci_execution_worker_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Database::BatchedBackgroundMigration::CiExecutionWorker,
+ :clean_gitlab_redis_shared_state,
+ feature_category: :database do
+ it_behaves_like 'batched background migrations execution worker'
+ end
diff --git a/spec/workers/database/batched_background_migration/main_execution_worker_spec.rb b/spec/workers/database/batched_background_migration/main_execution_worker_spec.rb
new file mode 100644
index 00000000000..42a3675f735
--- /dev/null
+++ b/spec/workers/database/batched_background_migration/main_execution_worker_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Database::BatchedBackgroundMigration::MainExecutionWorker,
+ :clean_gitlab_redis_shared_state,
+ feature_category: :database do
+ it_behaves_like 'batched background migrations execution worker'
+ end
diff --git a/spec/workers/delete_container_repository_worker_spec.rb b/spec/workers/delete_container_repository_worker_spec.rb
index a011457444a..6ad131b4c14 100644
--- a/spec/workers/delete_container_repository_worker_spec.rb
+++ b/spec/workers/delete_container_repository_worker_spec.rb
@@ -10,112 +10,10 @@ RSpec.describe DeleteContainerRepositoryWorker do
let(:worker) { described_class.new }
describe '#perform' do
- let(:user_id) { user.id }
- let(:repository_id) { repository.id }
+ subject(:perform) { worker.perform(user.id, repository.id) }
- subject(:perform) { worker.perform(user_id, repository_id) }
-
- it 'executes the destroy service' do
- expect_destroy_service_execution
-
- perform
- end
-
- context 'with an invalid user id' do
- let(:user_id) { -1 }
-
- it { expect { perform }.not_to raise_error }
- end
-
- context 'with an invalid repository id' do
- let(:repository_id) { -1 }
-
- it { expect { perform }.not_to raise_error }
- end
-
- context 'with a repository being migrated', :freeze_time do
- before do
- stub_application_setting(
- container_registry_pre_import_tags_rate: 0.5,
- container_registry_import_timeout: 10.minutes.to_i
- )
- end
-
- shared_examples 'destroying the repository' do
- it 'does destroy the repository' do
- expect_next_found_instance_of(ContainerRepository) do |container_repository|
- expect(container_repository).not_to receive(:tags_count)
- end
- expect(described_class).not_to receive(:perform_in)
- expect_destroy_service_execution
-
- perform
- end
- end
-
- shared_examples 'not re enqueuing job if feature flag is disabled' do
- before do
- stub_feature_flags(container_registry_migration_phase2_delete_container_repository_worker_support: false)
- end
-
- it_behaves_like 'destroying the repository'
- end
-
- context 'with migration state set to pre importing' do
- let_it_be(:repository) { create(:container_repository, :pre_importing) }
-
- let(:tags_count) { 60 }
- let(:delay) { (tags_count * 0.5).seconds + 10.minutes + described_class::FIXED_DELAY }
-
- it 'does not destroy the repository and re enqueue the job' do
- expect_next_found_instance_of(ContainerRepository) do |container_repository|
- expect(container_repository).to receive(:tags_count).and_return(tags_count)
- end
- expect(described_class).to receive(:perform_in).with(delay.from_now)
- expect(worker).to receive(:log_extra_metadata_on_done).with(:delete_postponed, delay)
- expect(::Projects::ContainerRepository::DestroyService).not_to receive(:new)
-
- perform
- end
-
- it_behaves_like 'not re enqueuing job if feature flag is disabled'
- end
-
- %i[pre_import_done importing import_aborted].each do |migration_state|
- context "with migration state set to #{migration_state}" do
- let_it_be(:repository) { create(:container_repository, migration_state) }
-
- let(:delay) { 10.minutes + described_class::FIXED_DELAY }
-
- it 'does not destroy the repository and re enqueue the job' do
- expect_next_found_instance_of(ContainerRepository) do |container_repository|
- expect(container_repository).not_to receive(:tags_count)
- end
- expect(described_class).to receive(:perform_in).with(delay.from_now)
- expect(worker).to receive(:log_extra_metadata_on_done).with(:delete_postponed, delay)
- expect(::Projects::ContainerRepository::DestroyService).not_to receive(:new)
-
- perform
- end
-
- it_behaves_like 'not re enqueuing job if feature flag is disabled'
- end
- end
-
- %i[default import_done import_skipped].each do |migration_state|
- context "with migration state set to #{migration_state}" do
- let_it_be(:repository) { create(:container_repository, migration_state) }
-
- it_behaves_like 'destroying the repository'
- it_behaves_like 'not re enqueuing job if feature flag is disabled'
- end
- end
- end
-
- def expect_destroy_service_execution
- service = instance_double(Projects::ContainerRepository::DestroyService)
- expect(service).to receive(:execute)
- expect(Projects::ContainerRepository::DestroyService).to receive(:new).with(project, user).and_return(service)
+ it 'is a no op' do
+ expect { subject }.to not_change { ContainerRepository.count }
end
end
end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index e705ca28e54..788f5d8222c 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -139,6 +139,7 @@ RSpec.describe 'Every Sidekiq worker' do
'BuildQueueWorker' => 3,
'BuildSuccessWorker' => 3,
'BulkImportWorker' => false,
+ 'BulkImports::ExportRequestWorker' => 5,
'BulkImports::EntityWorker' => false,
'BulkImports::PipelineWorker' => false,
'Chaos::CpuSpinWorker' => 3,
@@ -193,6 +194,8 @@ RSpec.describe 'Every Sidekiq worker' do
'CreateGithubWebhookWorker' => 3,
'CreateNoteDiffFileWorker' => 3,
'CreatePipelineWorker' => 3,
+ 'Database::BatchedBackgroundMigration::CiExecutionWorker' => 0,
+ 'Database::BatchedBackgroundMigration::MainExecutionWorker' => 0,
'DeleteContainerRepositoryWorker' => 3,
'DeleteDiffFilesWorker' => 3,
'DeleteMergedBranchesWorker' => 3,
@@ -286,6 +289,9 @@ RSpec.describe 'Every Sidekiq worker' do
'Gitlab::GithubImport::Stage::ImportPullRequestsReviewsWorker' => 5,
'Gitlab::GithubImport::Stage::ImportPullRequestsWorker' => 5,
'Gitlab::GithubImport::Stage::ImportRepositoryWorker' => 5,
+ 'Gitlab::GithubGistsImport::ImportGistWorker' => 5,
+ 'Gitlab::GithubGistsImport::StartImportWorker' => 5,
+ 'Gitlab::GithubGistsImport::FinishImportWorker' => 5,
'Gitlab::JiraImport::AdvanceStageWorker' => 5,
'Gitlab::JiraImport::ImportIssueWorker' => 5,
'Gitlab::JiraImport::Stage::FinishImportWorker' => 5,
@@ -340,6 +346,7 @@ RSpec.describe 'Every Sidekiq worker' do
'MergeRequestMergeabilityCheckWorker' => 3,
'MergeRequestResetApprovalsWorker' => 3,
'MergeRequests::AssigneesChangeWorker' => 3,
+ 'MergeRequests::CaptureSuggestedReviewersAcceptedWorker' => 3,
'MergeRequests::CreatePipelineWorker' => 3,
'MergeRequests::DeleteSourceBranchWorker' => 3,
'MergeRequests::FetchSuggestedReviewersWorker' => 3,
@@ -366,7 +373,6 @@ RSpec.describe 'Every Sidekiq worker' do
'ObjectPool::DestroyWorker' => 3,
'ObjectPool::JoinWorker' => 3,
'ObjectPool::ScheduleJoinWorker' => 3,
- 'ObjectStorage::BackgroundMoveWorker' => 5,
'ObjectStorage::MigrateUploadsWorker' => 3,
'Onboarding::CreateLearnGitlabWorker' => 3,
'Packages::CleanupPackageFileWorker' => 0,
@@ -388,6 +394,7 @@ RSpec.describe 'Every Sidekiq worker' do
'PipelineProcessWorker' => 3,
'PostReceive' => 3,
'ProcessCommitWorker' => 3,
+ 'ProductAnalytics::InitializeAnalyticsWorker' => 3,
'ProjectCacheWorker' => 3,
'ProjectDestroyWorker' => 3,
'ProjectExportWorker' => false,
diff --git a/spec/workers/flush_counter_increments_worker_spec.rb b/spec/workers/flush_counter_increments_worker_spec.rb
index 14b49b97ac3..83670acf4b6 100644
--- a/spec/workers/flush_counter_increments_worker_spec.rb
+++ b/spec/workers/flush_counter_increments_worker_spec.rb
@@ -12,29 +12,32 @@ RSpec.describe FlushCounterIncrementsWorker, :counter_attribute do
subject { worker.perform(model.class.name, model.id, attribute) }
- it 'flushes increments to database' do
+ it 'commits increments to database' do
expect(model.class).to receive(:find_by_id).and_return(model)
- expect(model)
- .to receive(:flush_increments_to_database!)
- .with(attribute)
- .and_call_original
+ expect_next_instance_of(Gitlab::Counters::BufferedCounter, model, attribute) do |service|
+ expect(service).to receive(:commit_increment!)
+ end
subject
end
context 'when model class does not exist' do
- subject { worker.perform('non-existend-model') }
+ subject { worker.perform('NonExistentModel', 1, attribute) }
it 'does nothing' do
- expect(worker).not_to receive(:in_lock)
+ expect(Gitlab::Counters::BufferedCounter).not_to receive(:new)
+
+ subject
end
end
context 'when record does not exist' do
- subject { worker.perform(model.class.name, model.id + 100, attribute) }
+ subject { worker.perform(model.class.name, non_existing_record_id, attribute) }
it 'does nothing' do
- expect(worker).not_to receive(:in_lock)
+ expect(Gitlab::Counters::BufferedCounter).not_to receive(:new)
+
+ subject
end
end
end
diff --git a/spec/workers/gitlab/export/prune_project_export_jobs_worker_spec.rb b/spec/workers/gitlab/export/prune_project_export_jobs_worker_spec.rb
new file mode 100644
index 00000000000..eded07c7a2f
--- /dev/null
+++ b/spec/workers/gitlab/export/prune_project_export_jobs_worker_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Export::PruneProjectExportJobsWorker, feature_category: :importers do
+ let_it_be(:old_job_1) { create(:project_export_job, updated_at: 37.months.ago) }
+ let_it_be(:old_job_2) { create(:project_export_job, updated_at: 12.months.ago) }
+ let_it_be(:old_job_3) { create(:project_export_job, updated_at: 8.days.ago) }
+ let_it_be(:fresh_job_1) { create(:project_export_job, updated_at: 1.day.ago) }
+ let_it_be(:fresh_job_2) { create(:project_export_job, updated_at: 2.days.ago) }
+ let_it_be(:fresh_job_3) { create(:project_export_job, updated_at: 6.days.ago) }
+
+ let_it_be(:old_relation_export_1) { create(:project_relation_export, project_export_job_id: old_job_1.id) }
+ let_it_be(:old_relation_export_2) { create(:project_relation_export, project_export_job_id: old_job_2.id) }
+ let_it_be(:old_relation_export_3) { create(:project_relation_export, project_export_job_id: old_job_3.id) }
+ let_it_be(:fresh_relation_export_1) { create(:project_relation_export, project_export_job_id: fresh_job_1.id) }
+
+ let_it_be(:old_upload_1) { create(:relation_export_upload, project_relation_export_id: old_relation_export_1.id) }
+ let_it_be(:old_upload_2) { create(:relation_export_upload, project_relation_export_id: old_relation_export_2.id) }
+ let_it_be(:old_upload_3) { create(:relation_export_upload, project_relation_export_id: old_relation_export_3.id) }
+ let_it_be(:fresh_upload_1) { create(:relation_export_upload, project_relation_export_id: fresh_relation_export_1.id) }
+
+ subject(:worker) { described_class.new }
+
+ describe '#perform' do
+ include_examples 'an idempotent worker' do
+ it 'prunes jobs and associations older than 7 days' do
+ expect { perform_multiple }.to change { ProjectExportJob.count }.by(-3)
+ expect(ProjectExportJob.find_by(id: old_job_1.id)).to be_nil
+ expect(ProjectExportJob.find_by(id: old_job_2.id)).to be_nil
+ expect(ProjectExportJob.find_by(id: old_job_3.id)).to be_nil
+
+ expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_1.id)).to be_nil
+ expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_2.id)).to be_nil
+ expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_3.id)).to be_nil
+
+ expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_1.id)).to be_nil
+ expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_2.id)).to be_nil
+ expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_3.id)).to be_nil
+ end
+
+ it 'leaves fresh jobs and associations' do
+ perform_multiple
+ expect(fresh_job_1.reload).to be_present
+ expect(fresh_job_2.reload).to be_present
+ expect(fresh_job_3.reload).to be_present
+ expect(fresh_relation_export_1.reload).to be_present
+ expect(fresh_upload_1.reload).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_gists_import/finish_import_worker_spec.rb b/spec/workers/gitlab/github_gists_import/finish_import_worker_spec.rb
new file mode 100644
index 00000000000..c4c19f2f9c5
--- /dev/null
+++ b/spec/workers/gitlab/github_gists_import/finish_import_worker_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubGistsImport::FinishImportWorker, feature_category: :importer do
+ subject(:worker) { described_class.new }
+
+ let_it_be(:user) { create(:user) }
+
+ describe '#perform', :aggregate_failures do
+ context 'when there are no remaining jobs' do
+ it 'marks import status as finished' do
+ waiter = instance_double(Gitlab::JobWaiter, key: :key, jobs_remaining: 0)
+ expect(Gitlab::JobWaiter).to receive(:new).and_return(waiter)
+ expect(waiter).to receive(:wait).with(described_class::BLOCKING_WAIT_TIME)
+ expect_next_instance_of(Gitlab::GithubGistsImport::Status) do |status|
+ expect(status).to receive(:finish!)
+ end
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:info)
+ .with(user_id: user.id, message: 'GitHub Gists import finished')
+
+ worker.perform(user.id, waiter.key, waiter.jobs_remaining)
+ end
+ end
+
+ context 'when there are remaining jobs' do
+ it 'reschedules the worker' do
+ waiter = instance_double(Gitlab::JobWaiter, key: :key, jobs_remaining: 2)
+ expect(Gitlab::JobWaiter).to receive(:new).and_return(waiter)
+ expect(waiter).to receive(:wait).with(described_class::BLOCKING_WAIT_TIME)
+ expect(described_class).to receive(:perform_in)
+ .with(described_class::INTERVAL, user.id, waiter.key, waiter.jobs_remaining)
+
+ worker.perform(user.id, waiter.key, waiter.jobs_remaining)
+ end
+ end
+ end
+
+ describe '.sidekiq_retries_exhausted' do
+ it 'sets status to failed' do
+ job = { 'args' => [user.id, 'some_key', '1'], 'jid' => '123' }
+
+ expect_next_instance_of(Gitlab::GithubGistsImport::Status) do |status|
+ expect(status).to receive(:fail!)
+ end
+
+ described_class.sidekiq_retries_exhausted_block.call(job)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb b/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb
new file mode 100644
index 00000000000..dfc5084bb10
--- /dev/null
+++ b/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :importer do
+ subject { described_class.new }
+
+ let_it_be(:user) { create(:user) }
+ let(:token) { 'token' }
+ let(:gist_hash) do
+ {
+ id: '055b70',
+ git_pull_url: 'https://gist.github.com/foo/bar.git',
+ files: {
+ 'random.txt': {
+ filename: 'random.txt',
+ type: 'text/plain',
+ language: 'Text',
+ raw_url: 'https://gist.githubusercontent.com/user_name/055b70/raw/66a7be0d/random.txt',
+ size: 166903
+ }
+ },
+ is_public: false,
+ created_at: '2022-09-06T11:38:18Z',
+ updated_at: '2022-09-06T11:38:18Z',
+ description: 'random text'
+ }
+ end
+
+ let(:importer) { instance_double('Gitlab::GithubGistsImport::Importer::GistImporter') }
+ let(:importer_result) { instance_double('ServiceResponse', success?: true) }
+ let(:gist_object) do
+ instance_double('Gitlab::GithubGistsImport::Representation::Gist',
+ gist_hash.merge(github_identifiers: { id: '055b70' }, truncated_title: 'random text', visibility_level: 0))
+ end
+
+ let(:log_attributes) do
+ {
+ 'user_id' => user.id,
+ 'github_identifiers' => { 'id': gist_object.id },
+ 'class' => 'Gitlab::GithubGistsImport::ImportGistWorker',
+ 'correlation_id' => 'new-correlation-id',
+ 'jid' => nil,
+ 'job_status' => 'running',
+ 'queue' => 'github_gists_importer:github_gists_import_import_gist'
+ }
+ end
+
+ describe '#perform' do
+ before do
+ allow(Gitlab::GithubGistsImport::Representation::Gist)
+ .to receive(:from_json_hash)
+ .with(gist_hash)
+ .and_return(gist_object)
+
+ allow(Gitlab::GithubGistsImport::Importer::GistImporter)
+ .to receive(:new)
+ .with(gist_object, user.id)
+ .and_return(importer)
+
+ allow(Gitlab::ApplicationContext).to receive(:current).and_return('correlation_id' => 'new-correlation-id')
+ allow(described_class).to receive(:queue).and_return('github_gists_importer:github_gists_import_import_gist')
+ end
+
+ context 'when success' do
+ it 'imports gist' do
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:info)
+ .with(log_attributes.merge('message' => 'start importer'))
+ expect(importer).to receive(:execute).and_return(importer_result)
+ expect(Gitlab::JobWaiter).to receive(:notify).with('some_key', subject.jid)
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:info)
+ .with(log_attributes.merge('message' => 'importer finished'))
+
+ subject.perform(user.id, gist_hash, 'some_key')
+ end
+ end
+
+ context 'when importer raised an error' do
+ it 'raises an error' do
+ exception = StandardError.new('_some_error_')
+
+ expect(importer).to receive(:execute).and_raise(exception)
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:error)
+ .with(log_attributes.merge('message' => 'importer failed', 'error.message' => '_some_error_'))
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+
+ expect { subject.perform(user.id, gist_hash, 'some_key') }.to raise_error(StandardError)
+ end
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_gists_import/start_import_worker_spec.rb b/spec/workers/gitlab/github_gists_import/start_import_worker_spec.rb
new file mode 100644
index 00000000000..523b7463a9d
--- /dev/null
+++ b/spec/workers/gitlab/github_gists_import/start_import_worker_spec.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubGistsImport::StartImportWorker, feature_category: :importer do
+ subject(:worker) { described_class.new }
+
+ let_it_be(:user) { create(:user) }
+ let(:token) { Gitlab::CryptoHelper.aes256_gcm_encrypt('token') }
+ let(:importer) { instance_double(Gitlab::GithubGistsImport::Importer::GistsImporter) }
+ let(:waiter) { instance_double(Gitlab::JobWaiter, key: :key, jobs_remaining: 1) }
+ let(:importer_context) { Struct.new(:success?, :error, :waiter, :next_attempt_in, keyword_init: true) }
+ let(:log_attributes) do
+ {
+ 'user_id' => user.id,
+ 'class' => described_class.name,
+ 'correlation_id' => 'new-correlation-id',
+ 'jid' => nil,
+ 'job_status' => 'running',
+ 'queue' => 'github_gists_importer:github_gists_import_start_import'
+ }
+ end
+
+ describe '#perform', :aggregate_failures do
+ before do
+ allow(Gitlab::GithubImport::Logger)
+ .to receive(:info)
+ .with(log_attributes.merge('message' => 'starting importer'))
+
+ allow(Gitlab::ApplicationContext).to receive(:current).and_return('correlation_id' => 'new-correlation-id')
+ allow(described_class).to receive(:queue).and_return('github_gists_importer:github_gists_import_start_import')
+ end
+
+ context 'when import was successfull' do
+ it 'imports all the gists' do
+ expect(Gitlab::CryptoHelper)
+ .to receive(:aes256_gcm_decrypt)
+ .with(token)
+ .and_call_original
+
+ expect(Gitlab::GithubGistsImport::Importer::GistsImporter)
+ .to receive(:new)
+ .with(user, 'token')
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+ .and_return(importer_context.new(success?: true, waiter: waiter))
+
+ expect(Gitlab::GithubGistsImport::FinishImportWorker)
+ .to receive(:perform_async)
+ .with(user.id, waiter.key, waiter.jobs_remaining)
+
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:info)
+ .with(log_attributes.merge('message' => 'importer finished'))
+
+ worker.perform(user.id, token)
+ end
+ end
+
+ context 'when importer returns an error' do
+ it 'raises an error' do
+ exception = StandardError.new('_some_error_')
+ importer_result = importer_context.new(success?: false, error: exception)
+
+ expect_next_instance_of(Gitlab::GithubGistsImport::Importer::GistsImporter) do |importer|
+ expect(importer).to receive(:execute).and_return(importer_result)
+ end
+
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:error)
+ .with(log_attributes.merge('message' => 'import failed', 'error.message' => exception.message))
+
+ expect { worker.perform(user.id, token) }.to raise_error(StandardError)
+ end
+ end
+
+ context 'when rate limit is reached' do
+ it 'reschedules worker' do
+ exception = Gitlab::GithubImport::RateLimitError.new
+ importer_result = importer_context.new(success?: false, error: exception, next_attempt_in: 5)
+
+ expect_next_instance_of(Gitlab::GithubGistsImport::Importer::GistsImporter) do |importer|
+ expect(importer).to receive(:execute).and_return(importer_result)
+ end
+
+ expect(Gitlab::GithubImport::Logger)
+ .to receive(:info)
+ .with(log_attributes.merge('message' => 'rate limit reached'))
+
+ expect(described_class).to receive(:perform_in).with(5, user.id, token)
+
+ worker.perform(user.id, token)
+ end
+ end
+ end
+
+ describe '.sidekiq_retries_exhausted' do
+ it 'sets status to failed' do
+ job = { 'args' => [user.id, token], 'jid' => '123' }
+
+ expect_next_instance_of(Gitlab::GithubGistsImport::Status) do |status|
+ expect(status).to receive(:fail!)
+ end
+
+ described_class.sidekiq_retries_exhausted_block.call(job)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
index 15bc55c1526..c92741e8f10 100644
--- a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe Gitlab::GithubImport::ImportDiffNoteWorker do
hash = {
'noteable_id' => 42,
'github_id' => 42,
+ 'html_url' => 'https://github.com/foo/bar/pull/42',
'path' => 'README.md',
'commit_id' => '123abc',
'diff_hunk' => "@@ -1 +1 @@\n-Hello\n+Hello world",
diff --git a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
index 695e21f4733..0244e69b7b6 100644
--- a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::JiraImport::ImportIssueWorker do
describe '#perform', :clean_gitlab_redis_cache do
let(:assignee_ids) { [user.id] }
let(:issue_attrs) do
- build(:issue, project_id: project.id, title: 'jira issue')
+ build(:issue, project_id: project.id, namespace_id: project.project_namespace_id, title: 'jira issue')
.as_json.merge(
'label_ids' => [jira_issue_label_1.id, jira_issue_label_2.id], 'assignee_ids' => assignee_ids
).except('issue_type')
@@ -71,6 +71,7 @@ RSpec.describe Gitlab::JiraImport::ImportIssueWorker do
expect(issue.title).to eq('jira issue')
expect(issue.project).to eq(project)
+ expect(issue.namespace).to eq(project.project_namespace)
expect(issue.labels).to match_array([label, jira_issue_label_1, jira_issue_label_2])
expect(issue.assignees).to eq([user])
end
diff --git a/spec/workers/gitlab_shell_worker_spec.rb b/spec/workers/gitlab_shell_worker_spec.rb
index a5419291d35..838f2ef4ba4 100644
--- a/spec/workers/gitlab_shell_worker_spec.rb
+++ b/spec/workers/gitlab_shell_worker_spec.rb
@@ -18,29 +18,13 @@ RSpec.describe GitlabShellWorker, :sidekiq_inline do
end
describe 'all other commands' do
- context 'when verify_gitlab_shell_worker_method_names is enabled' do
- it 'raises ArgumentError' do
- allow_next_instance_of(described_class) do |job_instance|
- expect(job_instance).not_to receive(:gitlab_shell)
- end
-
- expect { described_class.perform_async('foo', 'bar', 'baz') }
- .to raise_error(ArgumentError, 'foo not allowed for GitlabShellWorker')
- end
- end
-
- context 'when verify_gitlab_shell_worker_method_names is disabled' do
- before do
- stub_feature_flags(verify_gitlab_shell_worker_method_names: false)
+ it 'raises ArgumentError' do
+ allow_next_instance_of(described_class) do |job_instance|
+ expect(job_instance).not_to receive(:gitlab_shell)
end
- it 'forwards the message to Gitlab::Shell' do
- expect_next_instance_of(Gitlab::Shell) do |instance|
- expect(instance).to receive('foo').with('bar', 'baz')
- end
-
- described_class.perform_async('foo', 'bar', 'baz')
- end
+ expect { described_class.perform_async('foo', 'bar', 'baz') }
+ .to raise_error(ArgumentError, 'foo not allowed for GitlabShellWorker')
end
end
end
diff --git a/spec/workers/incident_management/close_incident_worker_spec.rb b/spec/workers/incident_management/close_incident_worker_spec.rb
index b0d284ba5db..c96bb4a3d1e 100644
--- a/spec/workers/incident_management/close_incident_worker_spec.rb
+++ b/spec/workers/incident_management/close_incident_worker_spec.rb
@@ -17,14 +17,14 @@ RSpec.describe IncidentManagement::CloseIncidentWorker do
expect(service).to receive(:execute).with(issue, system_note: false).and_call_original
end
- expect { worker.perform(issue_id) }.to change(ResourceStateEvent, :count).by(1)
+ expect { worker.perform(issue_id) }.to change { ResourceStateEvent.count }.by(1)
end
shared_examples 'does not call the close issue service' do
specify do
expect(Issues::CloseService).not_to receive(:new)
- expect { worker.perform(issue_id) }.not_to change(ResourceStateEvent, :count)
+ expect { worker.perform(issue_id) }.not_to change { ResourceStateEvent.count }
end
end
@@ -58,7 +58,7 @@ RSpec.describe IncidentManagement::CloseIncidentWorker do
end
specify do
- expect { worker.perform(issue_id) }.not_to change(ResourceStateEvent, :count)
+ expect { worker.perform(issue_id) }.not_to change { ResourceStateEvent.count }
end
end
end
diff --git a/spec/workers/incident_management/pager_duty/process_incident_worker_spec.rb b/spec/workers/incident_management/pager_duty/process_incident_worker_spec.rb
index e2be91516b9..b81f1a575b5 100644
--- a/spec/workers/incident_management/pager_duty/process_incident_worker_spec.rb
+++ b/spec/workers/incident_management/pager_duty/process_incident_worker_spec.rb
@@ -22,14 +22,14 @@ RSpec.describe IncidentManagement::PagerDuty::ProcessIncidentWorker do
'assignees' => [{
'summary' => 'Laura Haley', 'url' => 'https://webdemo.pagerduty.com/users/P553OPV'
}],
- 'impacted_services' => [{
+ 'impacted_service' => {
'summary' => 'Production XDB Cluster', 'url' => 'https://webdemo.pagerduty.com/services/PN49J75'
- }]
+ }
}
end
it 'creates a GitLab issue' do
- expect { perform }.to change(Issue, :count).by(1)
+ expect { perform }.to change { Issue.count }.by(1)
end
end
@@ -41,7 +41,7 @@ RSpec.describe IncidentManagement::PagerDuty::ProcessIncidentWorker do
end
it 'does not create a GitLab issue' do
- expect { perform }.not_to change(Issue, :count)
+ expect { perform }.not_to change { Issue.count }
end
it 'logs a warning' do
diff --git a/spec/workers/issuable_export_csv_worker_spec.rb b/spec/workers/issuable_export_csv_worker_spec.rb
index a18d10ad3df..a5172d916b6 100644
--- a/spec/workers/issuable_export_csv_worker_spec.rb
+++ b/spec/workers/issuable_export_csv_worker_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe IssuableExportCsvWorker do
let(:issuable_type) { :issue }
it 'emails a CSV' do
- expect { subject }.to change(ActionMailer::Base.deliveries, :size).by(1)
+ expect { subject }.to change { ActionMailer::Base.deliveries.size }.by(1)
end
it 'ensures that project_id is passed to issues_finder' do
@@ -54,7 +54,7 @@ RSpec.describe IssuableExportCsvWorker do
let(:issuable_type) { :merge_request }
it 'emails a CSV' do
- expect { subject }.to change(ActionMailer::Base.deliveries, :size).by(1)
+ expect { subject }.to change { ActionMailer::Base.deliveries.size }.by(1)
end
it 'calls the MR export service' do
diff --git a/spec/workers/jira_connect/forward_event_worker_spec.rb b/spec/workers/jira_connect/forward_event_worker_spec.rb
index 7de9952a1da..d3db07b8cb4 100644
--- a/spec/workers/jira_connect/forward_event_worker_spec.rb
+++ b/spec/workers/jira_connect/forward_event_worker_spec.rb
@@ -24,14 +24,14 @@ RSpec.describe JiraConnect::ForwardEventWorker do
expect(Atlassian::Jwt).to receive(:encode).with({ iss: client_key, qsh: 'some_qsh' }, shared_secret).and_return('auth_token')
expect(JiraConnect::RetryRequestWorker).to receive(:perform_async).with(event_url, 'auth_token')
- expect { perform }.to change(JiraConnectInstallation, :count).by(-1)
+ expect { perform }.to change { JiraConnectInstallation.count }.by(-1)
end
context 'when installation does not exist' do
let(:jira_connect_installation) { instance_double(JiraConnectInstallation, id: -1) }
it 'does nothing' do
- expect { perform }.not_to change(JiraConnectInstallation, :count)
+ expect { perform }.not_to change { JiraConnectInstallation.count }
end
end
@@ -39,7 +39,7 @@ RSpec.describe JiraConnect::ForwardEventWorker do
let!(:jira_connect_installation) { create(:jira_connect_installation) }
it 'forwards the event including the auth header' do
- expect { perform }.to change(JiraConnectInstallation, :count).by(-1)
+ expect { perform }.to change { JiraConnectInstallation.count }.by(-1)
expect(JiraConnect::RetryRequestWorker).not_to receive(:perform_async)
end
diff --git a/spec/workers/jira_connect/send_uninstalled_hook_worker_spec.rb b/spec/workers/jira_connect/send_uninstalled_hook_worker_spec.rb
new file mode 100644
index 00000000000..d8ca8dee54d
--- /dev/null
+++ b/spec/workers/jira_connect/send_uninstalled_hook_worker_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe JiraConnect::SendUninstalledHookWorker, feature_category: :integrations do
+ describe '#perform' do
+ let_it_be(:jira_connect_installation) { create(:jira_connect_installation) }
+ let(:instance_url) { 'http://example.com' }
+ let(:attempts) { 3 }
+ let(:service_response) { ServiceResponse.new(status: :success) }
+ let(:job_args) { [jira_connect_installation.id, instance_url] }
+
+ before do
+ allow(JiraConnectInstallations::ProxyLifecycleEventService).to receive(:execute).and_return(service_response)
+ end
+
+ include_examples 'an idempotent worker' do
+ it 'calls the ProxyLifecycleEventService service' do
+ expect(JiraConnectInstallations::ProxyLifecycleEventService).to receive(:execute).with(
+ jira_connect_installation,
+ :uninstalled,
+ instance_url
+ ).twice
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/workers/mail_scheduler/notification_service_worker_spec.rb b/spec/workers/mail_scheduler/notification_service_worker_spec.rb
index 3c17025c152..482d99a43c2 100644
--- a/spec/workers/mail_scheduler/notification_service_worker_spec.rb
+++ b/spec/workers/mail_scheduler/notification_service_worker_spec.rb
@@ -53,31 +53,15 @@ RSpec.describe MailScheduler::NotificationServiceWorker do
end
context 'when the method is not allowed' do
- context 'when verify_mail_scheduler_notification_service_worker_method_names is enabled' do
- it 'raises ArgumentError' do
- expect(worker.notification_service).not_to receive(:async)
- expect(worker.notification_service).not_to receive(:foo)
+ it 'raises ArgumentError' do
+ expect(worker.notification_service).not_to receive(:async)
+ expect(worker.notification_service).not_to receive(:foo)
- expect { worker.perform('async', *serialize(key)) }
- .to raise_error(ArgumentError, 'async not allowed for MailScheduler::NotificationServiceWorker')
+ expect { worker.perform('async', *serialize(key)) }
+ .to raise_error(ArgumentError, 'async not allowed for MailScheduler::NotificationServiceWorker')
- expect { worker.perform('foo', *serialize(key)) }
- .to raise_error(ArgumentError, 'foo not allowed for MailScheduler::NotificationServiceWorker')
- end
- end
-
- context 'when verify_mail_scheduler_notification_service_worker_method_names is disabled' do
- before do
- stub_feature_flags(verify_mail_scheduler_notification_service_worker_method_names: false)
- end
-
- it 'forwards the argument to the service' do
- expect(worker.notification_service).to receive(:async)
- expect(worker.notification_service).to receive(:foo)
-
- worker.perform('async', *serialize(key))
- worker.perform('foo', *serialize(key))
- end
+ expect { worker.perform('foo', *serialize(key)) }
+ .to raise_error(ArgumentError, 'foo not allowed for MailScheduler::NotificationServiceWorker')
end
end
end
diff --git a/spec/workers/merge_requests/delete_branch_worker_spec.rb b/spec/workers/merge_requests/delete_branch_worker_spec.rb
deleted file mode 100644
index 80ca8c061f5..00000000000
--- a/spec/workers/merge_requests/delete_branch_worker_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MergeRequests::DeleteBranchWorker do
- let_it_be(:merge_request) { create(:merge_request) }
- let_it_be(:user) { create(:user) }
-
- let(:branch) { merge_request.source_branch }
- let(:sha) { merge_request.source_branch_sha }
- let(:retarget_branch) { true }
- let(:worker) { described_class.new }
-
- describe '#perform' do
- context 'with a non-existing merge request' do
- it 'does nothing' do
- expect(::Branches::DeleteService).not_to receive(:new)
- worker.perform(non_existing_record_id, user.id, branch, retarget_branch)
- end
- end
-
- context 'with a non-existing user' do
- it 'does nothing' do
- expect(::Branches::DeleteService).not_to receive(:new)
-
- worker.perform(merge_request.id, non_existing_record_id, branch, retarget_branch)
- end
- end
-
- context 'with existing user and merge request' do
- it 'calls service to delete source branch' do
- expect_next_instance_of(::Branches::DeleteService) do |instance|
- expect(instance).to receive(:execute).with(branch)
- end
-
- worker.perform(merge_request.id, user.id, branch, retarget_branch)
- end
-
- context 'when retarget branch param is true' do
- it 'calls the retarget chain service' do
- expect_next_instance_of(::MergeRequests::RetargetChainService) do |instance|
- expect(instance).to receive(:execute).with(merge_request)
- end
-
- worker.perform(merge_request.id, user.id, branch, retarget_branch)
- end
- end
-
- context 'when retarget branch param is false' do
- let(:retarget_branch) { false }
-
- it 'does not call the retarget chain service' do
- expect(::MergeRequests::RetargetChainService).not_to receive(:new)
-
- worker.perform(merge_request.id, user.id, branch, retarget_branch)
- end
- end
- end
-
- it_behaves_like 'an idempotent worker' do
- let(:merge_request) { create(:merge_request) }
- let(:job_args) { [merge_request.id, sha, user.id, true] }
- end
- end
-end
diff --git a/spec/workers/merge_requests/delete_source_branch_worker_spec.rb b/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
index 2935d3ef5dc..a7e4ffad259 100644
--- a/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
+++ b/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
@@ -3,17 +3,24 @@
require 'spec_helper'
RSpec.describe MergeRequests::DeleteSourceBranchWorker do
- let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:user) { create(:user) }
+ let_it_be(:merge_request) { create(:merge_request, author: user) }
let(:sha) { merge_request.source_branch_sha }
let(:worker) { described_class.new }
describe '#perform' do
+ before do
+ allow_next_instance_of(::Projects::DeleteBranchWorker) do |instance|
+ allow(instance).to receive(:perform).with(merge_request.source_project.id, user.id,
+ merge_request.source_branch)
+ end
+ end
+
context 'when the add_delete_branch_worker feature flag is enabled' do
context 'with a non-existing merge request' do
it 'does nothing' do
- expect(::MergeRequests::DeleteBranchWorker).not_to receive(:perform_async)
+ expect(::Projects::DeleteBranchWorker).not_to receive(:new)
worker.perform(non_existing_record_id, sha, user.id)
end
@@ -21,7 +28,7 @@ RSpec.describe MergeRequests::DeleteSourceBranchWorker do
context 'with a non-existing user' do
it 'does nothing' do
- expect(::MergeRequests::DeleteBranchWorker).not_to receive(:perform_async)
+ expect(::Projects::DeleteBranchWorker).not_to receive(:new)
worker.perform(merge_request.id, sha, non_existing_record_id)
end
@@ -29,15 +36,17 @@ RSpec.describe MergeRequests::DeleteSourceBranchWorker do
context 'with existing user and merge request' do
it 'creates a new delete branch worker async' do
- expect(::MergeRequests::DeleteBranchWorker).to receive(:perform_async).with(merge_request.id, user.id,
- merge_request.source_branch, true)
+ expect_next_instance_of(::Projects::DeleteBranchWorker) do |instance|
+ expect(instance).to receive(:perform).with(merge_request.source_project.id, user.id,
+ merge_request.source_branch)
+ end
worker.perform(merge_request.id, sha, user.id)
end
context 'source branch sha does not match' do
it 'does nothing' do
- expect(::MergeRequests::DeleteBranchWorker).not_to receive(:perform_async)
+ expect(::Projects::DeleteBranchWorker).not_to receive(:new)
worker.perform(merge_request.id, 'new-source-branch-sha', user.id)
end
@@ -45,7 +54,6 @@ RSpec.describe MergeRequests::DeleteSourceBranchWorker do
end
it_behaves_like 'an idempotent worker' do
- let(:merge_request) { create(:merge_request) }
let(:job_args) { [merge_request.id, sha, user.id] }
end
end
@@ -117,7 +125,6 @@ RSpec.describe MergeRequests::DeleteSourceBranchWorker do
end
it_behaves_like 'an idempotent worker' do
- let(:merge_request) { create(:merge_request) }
let(:job_args) { [merge_request.id, sha, user.id] }
end
end
diff --git a/spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb b/spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb
index 11343f69d6f..491ea64cff1 100644
--- a/spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb
+++ b/spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb
@@ -10,23 +10,24 @@ RSpec.describe Metrics::Dashboard::PruneOldAnnotationsWorker do
describe '#perform' do
it 'removes all annotations older than cut off', :aggregate_failures do
- Timecop.freeze(now) do
+ travel_to(now) do
described_class.new.perform
expect(Metrics::Dashboard::Annotation.all).to match_array([one_day_old_annotation, two_weeks_old_annotation])
# is idempotent in the scope of 24h
expect { described_class.new.perform }.not_to change { Metrics::Dashboard::Annotation.all.to_a }
- travel_to(24.hours.from_now) do
- described_class.new.perform
- expect(Metrics::Dashboard::Annotation.all).to match_array([one_day_old_annotation])
- end
+ end
+
+ travel_to(now + 24.hours) do
+ described_class.new.perform
+ expect(Metrics::Dashboard::Annotation.all).to match_array([one_day_old_annotation])
end
end
context 'batch to be deleted is bigger than upper limit' do
it 'schedules second job to clear remaining records' do
- Timecop.freeze(now) do
+ travel_to(now) do
create(:metrics_dashboard_annotation, starting_at: 1.month.ago)
stub_const("#{described_class}::DELETE_LIMIT", 1)
diff --git a/spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb b/spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb
index f151780ffd7..4b670a753e7 100644
--- a/spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb
+++ b/spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Metrics::Dashboard::SyncDashboardsWorker do
describe ".perform" do
context 'with valid dashboard hash' do
it 'imports metrics' do
- expect { worker.perform(project.id) }.to change(PrometheusMetric, :count).by(3)
+ expect { worker.perform(project.id) }.to change { PrometheusMetric.count }.by(3)
end
it 'is idempotent' do
@@ -32,7 +32,7 @@ RSpec.describe Metrics::Dashboard::SyncDashboardsWorker do
end
it 'does not import metrics' do
- expect { worker.perform(project.id) }.not_to change(PrometheusMetric, :count)
+ expect { worker.perform(project.id) }.not_to change { PrometheusMetric.count }
end
it 'does not raise an error' do
diff --git a/spec/workers/namespaces/process_sync_events_worker_spec.rb b/spec/workers/namespaces/process_sync_events_worker_spec.rb
index 5e5179eab62..9f389089609 100644
--- a/spec/workers/namespaces/process_sync_events_worker_spec.rb
+++ b/spec/workers/namespaces/process_sync_events_worker_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Namespaces::ProcessSyncEventsWorker do
expect(described_class).to receive(:perform_async).at_least(:twice).and_call_original
expect do
described_class.perform_async
- end.to change(Namespaces::SyncEvent, :count).from(3).to(0)
+ end.to change { Namespaces::SyncEvent.count }.from(3).to(0)
end
end
@@ -44,11 +44,11 @@ RSpec.describe Namespaces::ProcessSyncEventsWorker do
end
it 'consumes all sync events' do
- expect { perform }.to change(Namespaces::SyncEvent, :count).from(5).to(0)
+ expect { perform }.to change { Namespaces::SyncEvent.count }.from(5).to(0)
end
it 'syncs namespace hierarchy traversal ids' do
- expect { perform }.to change(Ci::NamespaceMirror, :all).to contain_exactly(
+ expect { perform }.to change { Ci::NamespaceMirror.all }.to contain_exactly(
an_object_having_attributes(namespace_id: group1.id, traversal_ids: [group1.id]),
an_object_having_attributes(namespace_id: group2.id, traversal_ids: [group1.id, group2.id]),
an_object_having_attributes(namespace_id: group3.id, traversal_ids: [group1.id, group2.id, group3.id])
diff --git a/spec/workers/namespaces/root_statistics_worker_spec.rb b/spec/workers/namespaces/root_statistics_worker_spec.rb
index 30854415405..e047c94816f 100644
--- a/spec/workers/namespaces/root_statistics_worker_spec.rb
+++ b/spec/workers/namespaces/root_statistics_worker_spec.rb
@@ -92,7 +92,6 @@ RSpec.describe Namespaces::RootStatisticsWorker, '#perform' do
it_behaves_like 'worker with data consistency',
described_class,
- feature_flag: :root_statistics_worker_read_replica,
data_consistency: :sticky
it 'has the `until_executed` deduplicate strategy' do
diff --git a/spec/workers/namespaces/schedule_aggregation_worker_spec.rb b/spec/workers/namespaces/schedule_aggregation_worker_spec.rb
index f2fe53d6112..62f9be501cc 100644
--- a/spec/workers/namespaces/schedule_aggregation_worker_spec.rb
+++ b/spec/workers/namespaces/schedule_aggregation_worker_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Namespaces::ScheduleAggregationWorker, '#perform', :clean_gitlab_
expect do
worker.perform(group.id)
- end.not_to change(Namespace::AggregationSchedule, :count)
+ end.not_to change { Namespace::AggregationSchedule.count }
end
end
@@ -26,7 +26,7 @@ RSpec.describe Namespaces::ScheduleAggregationWorker, '#perform', :clean_gitlab_
expect do
worker.perform(group.id)
- end.to change(Namespace::AggregationSchedule, :count).by(1)
+ end.to change { Namespace::AggregationSchedule.count }.by(1)
expect(group.aggregation_schedule).to be_present
end
diff --git a/spec/workers/packages/debian/process_changes_worker_spec.rb b/spec/workers/packages/debian/process_changes_worker_spec.rb
index 93eba4bfa9a..fc482245ebe 100644
--- a/spec/workers/packages/debian/process_changes_worker_spec.rb
+++ b/spec/workers/packages/debian/process_changes_worker_spec.rb
@@ -78,10 +78,28 @@ RSpec.describe Packages::Debian::ProcessChangesWorker, type: :worker do
end
end
+ context 'without a distribution' do
+ before do
+ distribution.destroy!
+ end
+
+ it 'removes package file and log exception', :aggregate_failures do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
+ instance_of(ActiveRecord::RecordNotFound),
+ package_file_id: package_file_id,
+ user_id: user_id
+ )
+ expect { subject }
+ .to not_change { Packages::Package.count }
+ .and change { Packages::PackageFile.count }.by(-1)
+ .and change { incoming.package_files.count }.from(7).to(6)
+ end
+ end
+
context 'when the service raises an error' do
let(:package_file) { incoming.package_files.first }
- it 'removes package file', :aggregate_failures do
+ it 'removes package file and log exception', :aggregate_failures do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
instance_of(Packages::Debian::ExtractChangesMetadataService::ExtractionError),
package_file_id: package_file_id,
diff --git a/spec/workers/packages/debian/process_package_file_worker_spec.rb b/spec/workers/packages/debian/process_package_file_worker_spec.rb
new file mode 100644
index 00000000000..532bfb096a3
--- /dev/null
+++ b/spec/workers/packages/debian/process_package_file_worker_spec.rb
@@ -0,0 +1,138 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::ProcessPackageFileWorker, type: :worker, feature_category: :package_registry do
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, codename: 'unstable') }
+
+ let(:incoming) { create(:debian_incoming, project: distribution.project) }
+ let(:distribution_name) { distribution.codename }
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ let(:package_file_id) { package_file.id }
+ let(:user_id) { user.id }
+
+ subject { worker.perform(package_file_id, user_id, distribution_name, component_name) }
+
+ shared_examples 'returns early without error' do
+ it 'returns early without error' do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+ expect(::Packages::Debian::ProcessPackageFileService).not_to receive(:new)
+
+ subject
+ end
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:case_name, :expected_file_type, :file_name, :component_name) do
+ 'with a deb' | 'deb' | 'libsample0_1.2.3~alpha2_amd64.deb' | 'main'
+ 'with an udeb' | 'udeb' | 'sample-udeb_1.2.3~alpha2_amd64.udeb' | 'contrib'
+ end
+
+ with_them do
+ context 'with Debian package file' do
+ let(:package_file) { incoming.package_files.with_file_name(file_name).first }
+
+ context 'with mocked service' do
+ it 'calls ProcessPackageFileService' do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+ expect_next_instance_of(::Packages::Debian::ProcessPackageFileService) do |service|
+ expect(service).to receive(:execute)
+ .with(no_args)
+ end
+
+ subject
+ end
+ end
+
+ context 'with non existing user' do
+ let(:user_id) { non_existing_record_id }
+
+ it_behaves_like 'returns early without error'
+ end
+
+ context 'with nil user id' do
+ let(:user_id) { nil }
+
+ it_behaves_like 'returns early without error'
+ end
+
+ context 'when the service raises an error' do
+ let(:package_file) { incoming.package_files.with_file_name('sample_1.2.3~alpha2.tar.xz').first }
+
+ it 'removes package file', :aggregate_failures do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
+ instance_of(ArgumentError),
+ package_file_id: package_file_id,
+ user_id: user_id,
+ distribution_name: distribution_name,
+ component_name: component_name
+ )
+ expect { subject }
+ .to not_change(Packages::Package, :count)
+ .and change { Packages::PackageFile.count }.by(-1)
+ .and change { incoming.package_files.count }.from(7).to(6)
+
+ expect { package_file.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [package_file.id, user.id, distribution_name, component_name] }
+
+ it 'sets the Debian file type as deb', :aggregate_failures do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+
+ # Using subject inside this block will process the job multiple times
+ expect { subject }
+ .to change { Packages::Package.count }.from(1).to(2)
+ .and not_change(Packages::PackageFile, :count)
+ .and change { incoming.package_files.count }.from(7).to(6)
+ .and change {
+ package_file&.debian_file_metadatum&.reload&.file_type
+ }.from('unknown').to(expected_file_type)
+
+ created_package = Packages::Package.last
+ expect(created_package.name).to eq 'sample'
+ expect(created_package.version).to eq '1.2.3~alpha2'
+ expect(created_package.creator).to eq user
+ end
+ end
+ end
+ end
+
+ context 'with already processed package file' do
+ let_it_be(:package_file) { create(:debian_package_file) }
+
+ let(:component_name) { 'main' }
+
+ it_behaves_like 'returns early without error'
+ end
+
+ context 'with a deb' do
+ let(:package_file) { incoming.package_files.with_file_name('libsample0_1.2.3~alpha2_amd64.deb').first }
+ let(:component_name) { 'main' }
+
+ context 'with FIPS mode enabled', :fips_mode do
+ it 'raises an error' do
+ expect { subject }.to raise_error(::Packages::FIPS::DisabledError)
+ end
+ end
+
+ context 'with non existing package file' do
+ let(:package_file_id) { non_existing_record_id }
+
+ it_behaves_like 'returns early without error'
+ end
+
+ context 'with nil package file id' do
+ let(:package_file_id) { nil }
+
+ it_behaves_like 'returns early without error'
+ end
+ end
+ end
+end
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index 4a7db0eca56..d23907a8def 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -17,8 +17,12 @@ RSpec.describe PipelineScheduleWorker do
before do
stub_application_setting(auto_devops_enabled: false)
stub_ci_pipeline_to_return_yaml_file
+ end
- pipeline_schedule.update_column(:next_run_at, 1.day.ago)
+ around do |example|
+ travel_to(pipeline_schedule.next_run_at + 1.hour) do
+ example.run
+ end
end
context 'when the schedule is runnable by the user' do
@@ -26,16 +30,22 @@ RSpec.describe PipelineScheduleWorker do
project.add_maintainer(user)
end
- context 'when there is a scheduled pipeline within next_run_at' do
+ context 'when there is a scheduled pipeline within next_run_at', :sidekiq_inline do
shared_examples 'successful scheduling' do
- it 'creates a new pipeline', :sidekiq_might_not_need_inline do
+ it 'creates a new pipeline' do
expect { subject }.to change { project.ci_pipelines.count }.by(1)
- expect(Ci::Pipeline.last).to be_schedule
+ last_pipeline = project.ci_pipelines.last
+
+ expect(last_pipeline).to be_schedule
+ expect(last_pipeline.pipeline_schedule).to eq(pipeline_schedule)
+ end
+
+ it 'updates next_run_at' do
+ expect { subject }.to change { pipeline_schedule.reload.next_run_at }.by(1.day)
+ end
- pipeline_schedule.reload
- expect(pipeline_schedule.next_run_at).to be > Time.current
- expect(pipeline_schedule).to eq(project.ci_pipelines.last.pipeline_schedule)
- expect(pipeline_schedule).to be_active
+ it 'does not change active status' do
+ expect { subject }.not_to change { pipeline_schedule.reload.active? }.from(true)
end
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index d632ca39e44..210987555c9 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -275,30 +275,17 @@ RSpec.describe PostReceive do
expect { perform }.to change { counter.read(:pushes) }.by(1)
end
- it 'records correct payload with Snowplow event', :snowplow do
- stub_feature_flags(route_hll_to_snowplow_phase2: true)
-
- perform
-
- expect_snowplow_event(
- category: 'PostReceive',
- action: 'source_code_pushes',
- namespace: project.namespace,
- user: project.first_owner,
- project: project
- )
- end
-
- context 'when FF is disabled' do
- before do
- stub_feature_flags(route_hll_to_snowplow_phase2: false)
- end
-
- it 'doesnt emit snowplow events', :snowplow do
- perform
-
- expect_no_snowplow_event
- end
+ it_behaves_like 'Snowplow event tracking' do
+ let(:action) { :push }
+ let(:category) { described_class.name }
+ let(:namespace) { project.namespace }
+ let(:user) { project.creator }
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:label) { 'counts.source_code_pushes' }
+ let(:property) { 'source_code_pushes' }
+ let(:context) { [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: label).to_h] }
+
+ subject(:post_receive) { perform }
end
end
end
@@ -324,8 +311,8 @@ RSpec.describe PostReceive do
expect do
perform
project.reload
- end.to change(project, :last_activity_at)
- .and change(project, :last_repository_updated_at)
+ end.to change { project.last_activity_at }
+ .and change { project.last_repository_updated_at }
end
end
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 01c44399b0c..072c660bc2b 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe ProcessCommitWorker do
it 'creates Issue::CloseWorker jobs' do
expect do
worker.close_issues(project, user, user, commit, [issue])
- end.to change(Issues::CloseWorker.jobs, :size).by(1)
+ end.to change { Issues::CloseWorker.jobs.size }.by(1)
end
end
diff --git a/spec/workers/projects/delete_branch_worker_spec.rb b/spec/workers/projects/delete_branch_worker_spec.rb
new file mode 100644
index 00000000000..c1289f56929
--- /dev/null
+++ b/spec/workers/projects/delete_branch_worker_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+# rubocop: disable Gitlab/ServiceResponse
+
+require 'spec_helper'
+
+RSpec.describe Projects::DeleteBranchWorker, feature_category: :source_code_management do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ let(:branch) { 'master' }
+ let(:worker) { described_class.new }
+ let(:service_result) { ServiceResponse.success(message: 'placeholder', http_status: 200) }
+
+ before do
+ allow_next_instance_of(::Branches::DeleteService) do |instance|
+ allow(instance).to receive(:execute).with(branch).and_return(service_result)
+ end
+ end
+
+ describe '#perform' do
+ context 'when the branch does not exist' do
+ let(:branch) { 'non_existent_branch_name' }
+
+ it 'does nothing' do
+ expect(::Branches::DeleteService).not_to receive(:new)
+
+ worker.perform(project.id, user.id, branch)
+ end
+ end
+
+ context 'with a non-existing project' do
+ it 'does nothing' do
+ expect(::Branches::DeleteService).not_to receive(:new)
+
+ worker.perform(non_existing_record_id, user.id, branch)
+ end
+ end
+
+ context 'with a non-existing user' do
+ it 'does nothing' do
+ expect(::Branches::DeleteService).not_to receive(:new)
+
+ worker.perform(project.id, non_existing_record_id, branch)
+ end
+ end
+
+ context 'with existing user and project' do
+ it 'calls service to delete source branch' do
+ expect_next_instance_of(::Branches::DeleteService) do |instance|
+ expect(instance).to receive(:execute).with(branch).and_return(service_result)
+ end
+
+ worker.perform(project.id, user.id, branch)
+ end
+
+ context 'when delete service returns an error' do
+ let(:service_result) { ServiceResponse.error(message: 'placeholder', http_status: status_code) }
+
+ context 'when the status code is 400' do
+ let(:status_code) { 400 }
+
+ it 'tracks and raises the exception' do
+ expect_next_instance_of(::Branches::DeleteService) do |instance|
+ expect(instance).to receive(:execute).with(branch).and_return(service_result)
+ end
+
+ expect(service_result).to receive(:track_and_raise_exception).and_call_original
+
+ expect { worker.perform(project.id, user.id, branch) }.to raise_error(StandardError)
+ end
+ end
+
+ context 'when the status code is not 400' do
+ let(:status_code) { 405 }
+
+ it 'does not track the exception' do
+ expect_next_instance_of(::Branches::DeleteService) do |instance|
+ expect(instance).to receive(:execute).with(branch).and_return(service_result)
+ end
+
+ expect(service_result).not_to receive(:track_and_raise_exception)
+
+ expect { worker.perform(project.id, user.id, branch) }.not_to raise_error
+ end
+ end
+
+ context 'when track_and_raise_delete_source_errors is disabled' do
+ let(:status_code) { 400 }
+
+ before do
+ stub_feature_flags(track_and_raise_delete_source_errors: false)
+ end
+
+ it 'does not track the exception' do
+ expect_next_instance_of(::Branches::DeleteService) do |instance|
+ expect(instance).to receive(:execute).with(branch).and_return(service_result)
+ end
+
+ expect(service_result).not_to receive(:track_and_raise_exception)
+
+ expect { worker.perform(project.id, user.id, branch) }.not_to raise_error
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [project.id, user.id, branch] }
+ end
+ end
+ # rubocop: enable Gitlab/ServiceResponse
+end
diff --git a/spec/workers/projects/import_export/parallel_project_export_worker_spec.rb b/spec/workers/projects/import_export/parallel_project_export_worker_spec.rb
new file mode 100644
index 00000000000..d3ac0a34295
--- /dev/null
+++ b/spec/workers/projects/import_export/parallel_project_export_worker_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ImportExport::ParallelProjectExportWorker, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+
+ let(:export_job) { create(:project_export_job, :started) }
+ let(:after_export_strategy) { {} }
+ let(:job_args) { [export_job.id, user.id, after_export_strategy] }
+
+ before do
+ allow_next_instance_of(described_class) do |job|
+ allow(job).to receive(:jid) { SecureRandom.hex(8) }
+ end
+ end
+
+ describe '#perform' do
+ it_behaves_like 'an idempotent worker' do
+ it 'sets the export job status to finished' do
+ subject
+
+ expect(export_job.reload.finished?).to eq(true)
+ end
+ end
+
+ context 'when after export strategy does not exist' do
+ let(:after_export_strategy) { { 'klass' => 'InvalidStrategy' } }
+
+ it 'sets the export job status to failed' do
+ described_class.new.perform(*job_args)
+
+ expect(export_job.reload.failed?).to eq(true)
+ end
+ end
+ end
+
+ describe '.sidekiq_retries_exhausted' do
+ let(:job) { { 'args' => job_args, 'error_message' => 'Error message' } }
+
+ it 'sets export_job status to failed' do
+ described_class.sidekiq_retries_exhausted_block.call(job)
+
+ expect(export_job.reload.failed?).to eq(true)
+ end
+
+ it 'logs an error message' do
+ expect_next_instance_of(Gitlab::Export::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ hash_including(
+ message: 'Parallel project export error',
+ export_error: 'Error message'
+ )
+ )
+ end
+
+ described_class.sidekiq_retries_exhausted_block.call(job)
+ end
+ end
+end
diff --git a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb
index 50b5b0a6e7b..f3c6434dc85 100644
--- a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb
+++ b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Projects::InactiveProjectsDeletionCronWorker do
describe "#perform" do
subject(:worker) { described_class.new }
- let_it_be(:admin_user) { create(:user, :admin) }
+ let_it_be(:admin_bot) { create(:user, :admin_bot) }
let_it_be(:non_admin_user) { create(:user) }
let_it_be(:new_blank_project) do
create_project_with_statistics.tap do |project|
@@ -121,7 +121,7 @@ RSpec.describe Projects::InactiveProjectsDeletionCronWorker do
end
expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async)
- expect(::Projects::DestroyService).to receive(:new).with(inactive_large_project, admin_user, {})
+ expect(::Projects::DestroyService).to receive(:new).with(inactive_large_project, admin_bot, {})
.at_least(:once).and_call_original
worker.perform
diff --git a/spec/workers/projects/process_sync_events_worker_spec.rb b/spec/workers/projects/process_sync_events_worker_spec.rb
index 202942ce905..a10a4797b2c 100644
--- a/spec/workers/projects/process_sync_events_worker_spec.rb
+++ b/spec/workers/projects/process_sync_events_worker_spec.rb
@@ -26,11 +26,11 @@ RSpec.describe Projects::ProcessSyncEventsWorker do
end
it 'consumes all sync events' do
- expect { perform }.to change(Projects::SyncEvent, :count).from(2).to(0)
+ expect { perform }.to change { Projects::SyncEvent.count }.from(2).to(0)
end
it 'syncs project namespace id' do
- expect { perform }.to change(Ci::ProjectMirror, :all).to contain_exactly(
+ expect { perform }.to change { Ci::ProjectMirror.all }.to contain_exactly(
an_object_having_attributes(namespace_id: group.id)
)
end
diff --git a/spec/workers/releases/create_evidence_worker_spec.rb b/spec/workers/releases/create_evidence_worker_spec.rb
index 743f2abc8a7..7e3edcfe44a 100644
--- a/spec/workers/releases/create_evidence_worker_spec.rb
+++ b/spec/workers/releases/create_evidence_worker_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Releases::CreateEvidenceWorker do
expect(service).to receive(:execute).and_call_original
end
- expect { described_class.new.perform(release.id) }.to change(Releases::Evidence, :count).by(1)
+ expect { described_class.new.perform(release.id) }.to change { Releases::Evidence.count }.by(1)
end
it 'creates a new Evidence record with pipeline' do
@@ -21,6 +21,6 @@ RSpec.describe Releases::CreateEvidenceWorker do
expect(service).to receive(:execute).and_call_original
end
- expect { described_class.new.perform(release.id, pipeline.id) }.to change(Releases::Evidence, :count).by(1)
+ expect { described_class.new.perform(release.id, pipeline.id) }.to change { Releases::Evidence.count }.by(1)
end
end
diff --git a/spec/workers/releases/manage_evidence_worker_spec.rb b/spec/workers/releases/manage_evidence_worker_spec.rb
index 886fcd346eb..0004a4f4bfb 100644
--- a/spec/workers/releases/manage_evidence_worker_spec.rb
+++ b/spec/workers/releases/manage_evidence_worker_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Releases::ManageEvidenceWorker do
specify :sidekiq_inline do
aggregate_failures do
expect(::Releases::CreateEvidenceService).not_to receive(:execute)
- expect { described_class.new.perform }.to change(Releases::Evidence, :count).by(0)
+ expect { described_class.new.perform }.to change { Releases::Evidence.count }.by(0)
end
end
end
@@ -23,7 +23,7 @@ RSpec.describe Releases::ManageEvidenceWorker do
expect(service).to receive(:execute).and_call_original
end
- expect { described_class.new.perform }.to change(Releases::Evidence, :count).by(1)
+ expect { described_class.new.perform }.to change { Releases::Evidence.count }.by(1)
end
end
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index b8db262598b..0a37a296e7a 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -98,16 +98,6 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do
expect(project.reload.last_repository_check_failed).to eq(false)
end
- it 'does not create a wiki if the main repo does not exist at all' do
- project = create(:project, :repository)
- project.repository.raw.remove
- project.wiki.repository.raw.remove
-
- subject.perform(project.id)
-
- expect(TestEnv.storage_dir_exists?(project.repository_storage, project.wiki.path)).to eq(false)
- end
-
def create_push_event(project)
project.events.create!(action: :pushed, author_id: create(:user).id)
end
diff --git a/spec/workers/run_pipeline_schedule_worker_spec.rb b/spec/workers/run_pipeline_schedule_worker_spec.rb
index 5fa7c5d64db..4fdf6149435 100644
--- a/spec/workers/run_pipeline_schedule_worker_spec.rb
+++ b/spec/workers/run_pipeline_schedule_worker_spec.rb
@@ -3,6 +3,10 @@
require 'spec_helper'
RSpec.describe RunPipelineScheduleWorker do
+ it 'has an until_executed deduplicate strategy' do
+ expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
+ end
+
describe '#perform' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
@@ -11,6 +15,12 @@ RSpec.describe RunPipelineScheduleWorker do
let(:worker) { described_class.new }
+ around do |example|
+ travel_to(pipeline_schedule.next_run_at + 1.hour) do
+ example.run
+ end
+ end
+
context 'when a schedule not found' do
it 'does not call the Service' do
expect(Ci::CreatePipelineService).not_to receive(:new)
diff --git a/spec/workers/tasks_to_be_done/create_worker_spec.rb b/spec/workers/tasks_to_be_done/create_worker_spec.rb
index a158872273f..e884a71933e 100644
--- a/spec/workers/tasks_to_be_done/create_worker_spec.rb
+++ b/spec/workers/tasks_to_be_done/create_worker_spec.rb
@@ -24,13 +24,13 @@ RSpec.describe TasksToBeDone::CreateWorker do
.and_call_original
end
- expect { described_class.new.perform(*job_args) }.to change(Issue, :count).by(3)
+ expect { described_class.new.perform(*job_args) }.to change { Issue.count }.by(3)
end
end
include_examples 'an idempotent worker' do
it 'creates 3 task issues' do
- expect { subject }.to change(Issue, :count).by(3)
+ expect { subject }.to change { Issue.count }.by(3)
end
end
end
diff --git a/spec/workers/update_highest_role_worker_spec.rb b/spec/workers/update_highest_role_worker_spec.rb
index 0c8ee53da9a..cd127f26e95 100644
--- a/spec/workers/update_highest_role_worker_spec.rb
+++ b/spec/workers/update_highest_role_worker_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe UpdateHighestRoleWorker, :clean_gitlab_redis_shared_state do
describe '#perform' do
context 'when user is not found' do
it 'does not update or deletes any highest role', :aggregate_failures do
- expect { worker.perform(-1) }.not_to change(UserHighestRole, :count)
+ expect { worker.perform(-1) }.not_to change { UserHighestRole.count }
end
end
@@ -71,7 +71,7 @@ RSpec.describe UpdateHighestRoleWorker, :clean_gitlab_redis_shared_state do
it 'does not delete a highest role' do
user = create(:user, state: 'blocked')
- expect { worker.perform(user.id) }.not_to change(UserHighestRole, :count)
+ expect { worker.perform(user.id) }.not_to change { UserHighestRole.count }
end
end
end